@fluojs/platform-express 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 +21 -0
- package/README.ko.md +86 -0
- package/README.md +86 -0
- package/dist/adapter.d.ts +77 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +612 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/package.json +53 -0
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,86 @@
|
|
|
1
|
+
# @fluojs/platform-express
|
|
2
|
+
|
|
3
|
+
<p><a href="./README.md"><kbd>English</kbd></a> <strong><kbd>한국어</kbd></strong></p>
|
|
4
|
+
|
|
5
|
+
fluo 런타임을 위한 Express 기반 HTTP 어댑터 패키지입니다.
|
|
6
|
+
|
|
7
|
+
## 목차
|
|
8
|
+
|
|
9
|
+
- [설치](#설치)
|
|
10
|
+
- [사용 시점](#사용-시점)
|
|
11
|
+
- [빠른 시작](#빠른-시작)
|
|
12
|
+
- [주요 패턴](#주요-패턴)
|
|
13
|
+
- [공개 API 개요](#공개-api-개요)
|
|
14
|
+
- [관련 패키지](#관련-패키지)
|
|
15
|
+
- [예제 소스](#예제-소스)
|
|
16
|
+
|
|
17
|
+
## 설치
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install @fluojs/platform-express express
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## 사용 시점
|
|
24
|
+
|
|
25
|
+
fluo 애플리케이션의 기본 HTTP 엔진으로 Express를 사용하려는 경우에 이 패키지를 사용합니다. 이는 fluo의 데코레이터 기반 아키텍처 내에서 Express의 강력한 생태계, 성숙한 Node.js 서버 처리 및 친숙한 요청/응답 생명주기를 활용하는 데 유용합니다.
|
|
26
|
+
|
|
27
|
+
## 빠른 시작
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
import { createExpressAdapter } from '@fluojs/platform-express';
|
|
31
|
+
import { fluoFactory } from '@fluojs/runtime';
|
|
32
|
+
import { AppModule } from './app.module';
|
|
33
|
+
|
|
34
|
+
const app = await fluoFactory.create(AppModule, {
|
|
35
|
+
adapter: createExpressAdapter({ port: 3000 }),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
await app.listen();
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## 주요 패턴
|
|
42
|
+
|
|
43
|
+
### 스트리밍 응답 처리 (SSE)
|
|
44
|
+
Express 어댑터는 공유 `SseResponse` 유틸리티를 통해 Server-Sent Events(SSE)를 지원하며, Express 전용 스트림 처리를 추상화합니다.
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
@Get('events')
|
|
48
|
+
async streamEvents(@Res() res: FrameworkResponse) {
|
|
49
|
+
const events = new SseResponse();
|
|
50
|
+
events.send({ data: 'hello' });
|
|
51
|
+
return events;
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### 바디 파싱 및 멀티파트
|
|
56
|
+
`rawBody` 및 멀티파트 form-data 파싱을 즉시 사용할 수 있습니다. 어댑터를 직접 생성할 때는 멀티파트 제한을 두 번째 인자로 전달하고, `bootstrapExpressApplication(...)` 및 `runExpressApplication(...)`에서는 같은 설정을 `options.multipart` 아래에 전달하면 됩니다.
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
const adapter = createExpressAdapter(
|
|
60
|
+
{
|
|
61
|
+
port: 3000,
|
|
62
|
+
rawBody: true,
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
maxTotalSize: 10 * 1024 * 1024,
|
|
66
|
+
},
|
|
67
|
+
);
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## 공개 API 개요
|
|
71
|
+
|
|
72
|
+
- `createExpressAdapter(options)`: Express HTTP 어댑터를 위한 팩토리입니다.
|
|
73
|
+
- `bootstrapExpressApplication(module, options)`: 수동 제어를 위한 고급 부트스트랩 헬퍼입니다.
|
|
74
|
+
- `runExpressApplication(module, options)`: 시그널 연결을 포함한 빠른 시작을 위한 호환 헬퍼입니다. timeout/실패 시에는 해당 상태를 로그와 `process.exitCode`로 보고하고, 최종 프로세스 종료는 주변 호스트에 맡깁니다.
|
|
75
|
+
- `ExpressHttpApplicationAdapter`: 핵심 어댑터 구현 클래스입니다.
|
|
76
|
+
|
|
77
|
+
## 관련 패키지
|
|
78
|
+
|
|
79
|
+
- `@fluojs/runtime`: 핵심 프레임워크 런타임입니다.
|
|
80
|
+
- `@fluojs/platform-fastify`: 고성능을 지향하는 대안 어댑터입니다.
|
|
81
|
+
- `@fluojs/websockets`: Express를 위한 실시간 게이트웨이 지원을 제공합니다.
|
|
82
|
+
|
|
83
|
+
## 예제 소스
|
|
84
|
+
|
|
85
|
+
- `packages/platform-express/src/adapter.test.ts`
|
|
86
|
+
- `examples/minimal/src/main.ts` (Fastify 기반이지만 공유 `fluoFactory` 패턴을 보여줌)
|
package/README.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# @fluojs/platform-express
|
|
2
|
+
|
|
3
|
+
<p><strong><kbd>English</kbd></strong> <a href="./README.ko.md"><kbd>한국어</kbd></a></p>
|
|
4
|
+
|
|
5
|
+
Express-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
|
+
- [Public API Overview](#public-api-overview)
|
|
14
|
+
- [Related Packages](#related-packages)
|
|
15
|
+
- [Example Sources](#example-sources)
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install @fluojs/platform-express express
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## When to Use
|
|
24
|
+
|
|
25
|
+
Use this package when you want to run a fluo application using Express as the underlying HTTP engine. This is useful for leveraging Express's robust ecosystem, mature Node.js server handling, and familiar request/response lifecycle within the fluo decorator-based architecture.
|
|
26
|
+
|
|
27
|
+
## Quick Start
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
import { createExpressAdapter } from '@fluojs/platform-express';
|
|
31
|
+
import { fluoFactory } from '@fluojs/runtime';
|
|
32
|
+
import { AppModule } from './app.module';
|
|
33
|
+
|
|
34
|
+
const app = await fluoFactory.create(AppModule, {
|
|
35
|
+
adapter: createExpressAdapter({ port: 3000 }),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
await app.listen();
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Common Patterns
|
|
42
|
+
|
|
43
|
+
### Handling Streaming Responses (SSE)
|
|
44
|
+
The Express adapter supports Server-Sent Events (SSE) via the shared `SseResponse` utility, abstracting away the Express-specific stream handling.
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
@Get('events')
|
|
48
|
+
async streamEvents(@Res() res: FrameworkResponse) {
|
|
49
|
+
const events = new SseResponse();
|
|
50
|
+
events.send({ data: 'hello' });
|
|
51
|
+
return events;
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Body Parsing and Multipart
|
|
56
|
+
The adapter handles `rawBody` and multipart form-data parsing out of the box. When you construct the adapter directly, pass multipart limits as the second argument. `bootstrapExpressApplication(...)` and `runExpressApplication(...)` accept the same multipart settings under `options.multipart`.
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
const adapter = createExpressAdapter(
|
|
60
|
+
{
|
|
61
|
+
port: 3000,
|
|
62
|
+
rawBody: true,
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
maxTotalSize: 10 * 1024 * 1024,
|
|
66
|
+
},
|
|
67
|
+
);
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Public API Overview
|
|
71
|
+
|
|
72
|
+
- `createExpressAdapter(options)`: Factory for the Express HTTP adapter.
|
|
73
|
+
- `bootstrapExpressApplication(module, options)`: Advanced bootstrap helper for manual control.
|
|
74
|
+
- `runExpressApplication(module, options)`: Compatibility helper for quick startup with signal wiring. On timeout/failure it reports the condition through logging and `process.exitCode`, while leaving final process termination to the surrounding host.
|
|
75
|
+
- `ExpressHttpApplicationAdapter`: The core adapter implementation class.
|
|
76
|
+
|
|
77
|
+
## Related Packages
|
|
78
|
+
|
|
79
|
+
- `@fluojs/runtime`: Core framework runtime.
|
|
80
|
+
- `@fluojs/platform-fastify`: Alternative high-performance adapter.
|
|
81
|
+
- `@fluojs/websockets`: Real-time gateway support for Express.
|
|
82
|
+
|
|
83
|
+
## Example Sources
|
|
84
|
+
|
|
85
|
+
- `packages/platform-express/src/adapter.test.ts`
|
|
86
|
+
- `examples/minimal/src/main.ts` (Fastify-based, but demonstrates the shared `fluoFactory` pattern)
|
|
@@ -0,0 +1,77 @@
|
|
|
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, ApplicationLogger, CreateApplicationOptions, ModuleType, MultipartOptions, UploadedFile } from '@fluojs/runtime';
|
|
4
|
+
declare module '@fluojs/http' {
|
|
5
|
+
interface FrameworkRequest {
|
|
6
|
+
files?: UploadedFile[];
|
|
7
|
+
rawBody?: Uint8Array;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export interface ExpressAdapterOptions {
|
|
11
|
+
host?: string;
|
|
12
|
+
https?: HttpsServerOptions;
|
|
13
|
+
maxBodySize?: number;
|
|
14
|
+
port?: number;
|
|
15
|
+
rawBody?: boolean;
|
|
16
|
+
retryDelayMs?: number;
|
|
17
|
+
retryLimit?: number;
|
|
18
|
+
shutdownTimeoutMs?: number;
|
|
19
|
+
}
|
|
20
|
+
export type ExpressApplicationSignal = 'SIGINT' | 'SIGTERM';
|
|
21
|
+
export type CorsInput = false | string | string[] | CorsOptions;
|
|
22
|
+
export interface BootstrapExpressApplicationOptions extends Omit<CreateApplicationOptions, 'adapter' | 'logger' | 'middleware'> {
|
|
23
|
+
cors?: CorsInput;
|
|
24
|
+
globalPrefix?: string;
|
|
25
|
+
globalPrefixExclude?: readonly string[];
|
|
26
|
+
host?: string;
|
|
27
|
+
https?: HttpsServerOptions;
|
|
28
|
+
logger?: ApplicationLogger;
|
|
29
|
+
maxBodySize?: number;
|
|
30
|
+
middleware?: MiddlewareLike[];
|
|
31
|
+
multipart?: MultipartOptions;
|
|
32
|
+
port?: number;
|
|
33
|
+
rawBody?: boolean;
|
|
34
|
+
retryDelayMs?: number;
|
|
35
|
+
retryLimit?: number;
|
|
36
|
+
securityHeaders?: false | SecurityHeadersOptions;
|
|
37
|
+
shutdownTimeoutMs?: number;
|
|
38
|
+
}
|
|
39
|
+
export interface RunExpressApplicationOptions extends BootstrapExpressApplicationOptions {
|
|
40
|
+
forceExitTimeoutMs?: number;
|
|
41
|
+
shutdownSignals?: false | readonly ExpressApplicationSignal[];
|
|
42
|
+
}
|
|
43
|
+
interface ExpressListenTarget {
|
|
44
|
+
bindTarget: string;
|
|
45
|
+
url: string;
|
|
46
|
+
}
|
|
47
|
+
export declare class ExpressHttpApplicationAdapter implements HttpApplicationAdapter {
|
|
48
|
+
private readonly port;
|
|
49
|
+
private readonly host;
|
|
50
|
+
private readonly retryDelayMs;
|
|
51
|
+
private readonly retryLimit;
|
|
52
|
+
private readonly httpsOptions;
|
|
53
|
+
private readonly multipartOptions?;
|
|
54
|
+
private readonly maxBodySize;
|
|
55
|
+
private readonly preserveRawBody;
|
|
56
|
+
private readonly shutdownTimeoutMs;
|
|
57
|
+
private closeInFlight?;
|
|
58
|
+
private dispatcher?;
|
|
59
|
+
private readonly app;
|
|
60
|
+
private readonly requestResponseFactory;
|
|
61
|
+
private readonly server;
|
|
62
|
+
private readonly sockets;
|
|
63
|
+
constructor(port: number, host: string | undefined, retryDelayMs: number | undefined, retryLimit: number | undefined, httpsOptions: HttpsServerOptions | undefined, multipartOptions?: MultipartOptions | undefined, maxBodySize?: number, preserveRawBody?: boolean, shutdownTimeoutMs?: number);
|
|
64
|
+
getServer(): unknown;
|
|
65
|
+
getRealtimeCapability(): import("@fluojs/http").ServerBackedHttpAdapterRealtimeCapability;
|
|
66
|
+
getListenTarget(): ExpressListenTarget;
|
|
67
|
+
listen(dispatcher: Dispatcher): Promise<void>;
|
|
68
|
+
close(): Promise<void>;
|
|
69
|
+
private listenWithRetry;
|
|
70
|
+
private handleRequest;
|
|
71
|
+
}
|
|
72
|
+
export declare function createExpressAdapter(options?: ExpressAdapterOptions, multipartOptions?: MultipartOptions): HttpApplicationAdapter;
|
|
73
|
+
export declare function bootstrapExpressApplication(rootModule: ModuleType, options: BootstrapExpressApplicationOptions): Promise<Application>;
|
|
74
|
+
export declare function runExpressApplication(rootModule: ModuleType, options: RunExpressApplicationOptions): Promise<Application>;
|
|
75
|
+
export declare function isExpressMultipartTooLargeError(error: unknown): boolean;
|
|
76
|
+
export {};
|
|
77
|
+
//# 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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"}
|
package/dist/adapter.js
ADDED
|
@@ -0,0 +1,612 @@
|
|
|
1
|
+
import { createServer as createHttpServer } from 'node:http';
|
|
2
|
+
import { createServer as createHttpsServer } from 'node:https';
|
|
3
|
+
import { Readable } from 'node:stream';
|
|
4
|
+
import { URL } from 'node:url';
|
|
5
|
+
import express from 'express';
|
|
6
|
+
import { BadRequestException, createServerBackedHttpAdapterRealtimeCapability, createErrorResponse, HttpException, InternalServerErrorException, PayloadTooLargeException } from '@fluojs/http';
|
|
7
|
+
import { createNodeShutdownSignalRegistration, defaultNodeShutdownSignals } from '@fluojs/runtime/node';
|
|
8
|
+
import { parseMultipart } from '@fluojs/runtime/web';
|
|
9
|
+
import { bootstrapHttpAdapterApplication, runHttpAdapterApplication } from '@fluojs/runtime/internal/http-adapter';
|
|
10
|
+
import { dispatchWithRequestResponseFactory } from '@fluojs/runtime/internal/request-response-factory';
|
|
11
|
+
const DEFAULT_MAX_BODY_SIZE = 1 * 1024 * 1024;
|
|
12
|
+
const DEFAULT_SHUTDOWN_TIMEOUT_MS = 10_000;
|
|
13
|
+
export class ExpressHttpApplicationAdapter {
|
|
14
|
+
closeInFlight;
|
|
15
|
+
dispatcher;
|
|
16
|
+
app;
|
|
17
|
+
requestResponseFactory;
|
|
18
|
+
server;
|
|
19
|
+
sockets = new Set();
|
|
20
|
+
constructor(port, host, retryDelayMs = 150, retryLimit = 20, httpsOptions, multipartOptions, maxBodySize = DEFAULT_MAX_BODY_SIZE, preserveRawBody = false, shutdownTimeoutMs = DEFAULT_SHUTDOWN_TIMEOUT_MS) {
|
|
21
|
+
this.port = port;
|
|
22
|
+
this.host = host;
|
|
23
|
+
this.retryDelayMs = retryDelayMs;
|
|
24
|
+
this.retryLimit = retryLimit;
|
|
25
|
+
this.httpsOptions = httpsOptions;
|
|
26
|
+
this.multipartOptions = multipartOptions;
|
|
27
|
+
this.maxBodySize = maxBodySize;
|
|
28
|
+
this.preserveRawBody = preserveRawBody;
|
|
29
|
+
this.shutdownTimeoutMs = shutdownTimeoutMs;
|
|
30
|
+
this.app = express();
|
|
31
|
+
this.requestResponseFactory = createExpressRequestResponseFactory(this.multipartOptions, this.maxBodySize, this.preserveRawBody);
|
|
32
|
+
this.server = createExpressServer(this.httpsOptions, this.app);
|
|
33
|
+
this.app.use((request, response) => {
|
|
34
|
+
void this.handleRequest(request, response);
|
|
35
|
+
});
|
|
36
|
+
this.server.on('connection', socket => {
|
|
37
|
+
this.sockets.add(socket);
|
|
38
|
+
socket.once('close', () => {
|
|
39
|
+
this.sockets.delete(socket);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
getServer() {
|
|
44
|
+
return this.server;
|
|
45
|
+
}
|
|
46
|
+
getRealtimeCapability() {
|
|
47
|
+
return createServerBackedHttpAdapterRealtimeCapability(this.server);
|
|
48
|
+
}
|
|
49
|
+
getListenTarget() {
|
|
50
|
+
return resolveListenTarget(this.server.address() ?? null, this.port, this.host, this.httpsOptions !== undefined);
|
|
51
|
+
}
|
|
52
|
+
async listen(dispatcher) {
|
|
53
|
+
this.dispatcher = dispatcher;
|
|
54
|
+
await this.listenWithRetry();
|
|
55
|
+
}
|
|
56
|
+
async close() {
|
|
57
|
+
if (!this.server.listening) {
|
|
58
|
+
this.dispatcher = undefined;
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (!this.closeInFlight) {
|
|
62
|
+
const closePromise = closeServerWithDrain(this.server, this.sockets, this.shutdownTimeoutMs);
|
|
63
|
+
const closeInFlight = closePromise.finally(() => {
|
|
64
|
+
this.closeInFlight = undefined;
|
|
65
|
+
this.dispatcher = undefined;
|
|
66
|
+
});
|
|
67
|
+
this.closeInFlight = closeInFlight;
|
|
68
|
+
void closeInFlight.catch(() => {});
|
|
69
|
+
}
|
|
70
|
+
const closeInFlight = this.closeInFlight;
|
|
71
|
+
if (!closeInFlight) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
await waitForCloseWithTimeout(closeInFlight, this.shutdownTimeoutMs);
|
|
75
|
+
}
|
|
76
|
+
async listenWithRetry() {
|
|
77
|
+
for (let attempt = 0;; attempt++) {
|
|
78
|
+
try {
|
|
79
|
+
await listenServer(this.server, this.port, this.host);
|
|
80
|
+
return;
|
|
81
|
+
} catch (error) {
|
|
82
|
+
if (!isAddressInUseError(error) || attempt >= this.retryLimit) {
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
await closeServerSilently(this.server);
|
|
86
|
+
await delay(this.retryDelayMs);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async handleRequest(request, response) {
|
|
91
|
+
await dispatchWithRequestResponseFactory({
|
|
92
|
+
dispatcher: this.dispatcher,
|
|
93
|
+
dispatcherNotReadyMessage: 'Express adapter received a request before dispatcher binding completed.',
|
|
94
|
+
factory: this.requestResponseFactory,
|
|
95
|
+
rawRequest: request,
|
|
96
|
+
rawResponse: response
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function createExpressRequestResponseFactory(multipartOptions, maxBodySize = DEFAULT_MAX_BODY_SIZE, preserveRawBody = false) {
|
|
101
|
+
return {
|
|
102
|
+
async createRequest(request, signal) {
|
|
103
|
+
return createFrameworkRequest(request, signal, multipartOptions, maxBodySize, preserveRawBody);
|
|
104
|
+
},
|
|
105
|
+
createRequestSignal(response) {
|
|
106
|
+
return createRequestSignal(response);
|
|
107
|
+
},
|
|
108
|
+
createResponse(response) {
|
|
109
|
+
return createFrameworkResponse(response);
|
|
110
|
+
},
|
|
111
|
+
resolveRequestId(request) {
|
|
112
|
+
return resolveRequestIdFromHeaders(request.headers);
|
|
113
|
+
},
|
|
114
|
+
async writeErrorResponse(error, response, requestId) {
|
|
115
|
+
const httpError = toHttpException(error);
|
|
116
|
+
response.setStatus(httpError.status);
|
|
117
|
+
await response.send(createErrorResponse(httpError, requestId));
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
export function createExpressAdapter(options = {}, multipartOptions) {
|
|
122
|
+
return new ExpressHttpApplicationAdapter(resolvePort(options.port), options.host, options.retryDelayMs, options.retryLimit, options.https, multipartOptions, options.maxBodySize, options.rawBody, options.shutdownTimeoutMs);
|
|
123
|
+
}
|
|
124
|
+
export async function bootstrapExpressApplication(rootModule, options) {
|
|
125
|
+
return bootstrapHttpAdapterApplication(rootModule, options, createExpressAdapter(options, options.multipart));
|
|
126
|
+
}
|
|
127
|
+
export async function runExpressApplication(rootModule, options) {
|
|
128
|
+
const adapter = createExpressAdapter(options, options.multipart);
|
|
129
|
+
return runHttpAdapterApplication(rootModule, {
|
|
130
|
+
...options,
|
|
131
|
+
shutdownRegistration: createNodeShutdownSignalRegistration(options.shutdownSignals ?? defaultNodeShutdownSignals())
|
|
132
|
+
}, adapter);
|
|
133
|
+
}
|
|
134
|
+
function createFrameworkResponse(response) {
|
|
135
|
+
return {
|
|
136
|
+
committed: response.headersSent || response.writableEnded,
|
|
137
|
+
headers: {},
|
|
138
|
+
raw: response,
|
|
139
|
+
stream: createFrameworkResponseStream(response),
|
|
140
|
+
redirect(status, location) {
|
|
141
|
+
this.setStatus(status);
|
|
142
|
+
this.setHeader('Location', location);
|
|
143
|
+
this.committed = true;
|
|
144
|
+
response.redirect(status, location);
|
|
145
|
+
},
|
|
146
|
+
async send(body) {
|
|
147
|
+
if (response.writableEnded) {
|
|
148
|
+
this.committed = true;
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const existingContentType = response.getHeader('content-type');
|
|
152
|
+
const serialized = serializeResponseBody(body, typeof existingContentType === 'string' ? existingContentType : undefined);
|
|
153
|
+
if (!response.hasHeader('content-type') && serialized.defaultContentType) {
|
|
154
|
+
response.setHeader('content-type', serialized.defaultContentType);
|
|
155
|
+
}
|
|
156
|
+
this.committed = true;
|
|
157
|
+
response.send(serialized.payload);
|
|
158
|
+
},
|
|
159
|
+
setHeader(name, value) {
|
|
160
|
+
const lowerName = name.toLowerCase();
|
|
161
|
+
if (lowerName === 'set-cookie') {
|
|
162
|
+
const merged = mergeSetCookieHeader(response.getHeader(name), value);
|
|
163
|
+
response.setHeader(name, merged);
|
|
164
|
+
this.headers[name] = merged;
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
response.setHeader(name, value);
|
|
168
|
+
this.headers[name] = value;
|
|
169
|
+
},
|
|
170
|
+
setStatus(code) {
|
|
171
|
+
response.status(code);
|
|
172
|
+
this.statusCode = code;
|
|
173
|
+
this.statusSet = true;
|
|
174
|
+
},
|
|
175
|
+
statusCode: undefined,
|
|
176
|
+
statusSet: false
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
function createFrameworkResponseStream(response) {
|
|
180
|
+
return {
|
|
181
|
+
close() {
|
|
182
|
+
if (!response.writableEnded) {
|
|
183
|
+
response.end();
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
get closed() {
|
|
187
|
+
return response.writableEnded;
|
|
188
|
+
},
|
|
189
|
+
flush() {
|
|
190
|
+
response.flushHeaders?.();
|
|
191
|
+
},
|
|
192
|
+
onClose(listener) {
|
|
193
|
+
response.on('close', listener);
|
|
194
|
+
return () => {
|
|
195
|
+
response.removeListener('close', listener);
|
|
196
|
+
};
|
|
197
|
+
},
|
|
198
|
+
waitForDrain() {
|
|
199
|
+
if (response.writableEnded) {
|
|
200
|
+
return Promise.resolve();
|
|
201
|
+
}
|
|
202
|
+
return new Promise(resolve => {
|
|
203
|
+
response.once('drain', () => resolve());
|
|
204
|
+
});
|
|
205
|
+
},
|
|
206
|
+
write(chunk) {
|
|
207
|
+
return response.write(chunk);
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
async function createFrameworkRequest(request, signal, multipartOptions, maxBodySize = DEFAULT_MAX_BODY_SIZE, preserveRawBody = false) {
|
|
212
|
+
const rawUrl = request.originalUrl || request.url || '/';
|
|
213
|
+
const url = new URL(rawUrl, 'http://localhost');
|
|
214
|
+
const headers = normalizeHeaders(request.headers);
|
|
215
|
+
const contentType = readPrimaryHeaderValue(headers['content-type']);
|
|
216
|
+
const isMultipart = typeof contentType === 'string' && contentType.includes('multipart/form-data');
|
|
217
|
+
let body;
|
|
218
|
+
let files;
|
|
219
|
+
let rawBody;
|
|
220
|
+
if (isMultipart) {
|
|
221
|
+
const parsed = await parseMultipartRequest(request, multipartOptions);
|
|
222
|
+
body = parsed.fields;
|
|
223
|
+
files = parsed.files;
|
|
224
|
+
} else {
|
|
225
|
+
const bodyResult = await readRequestBody(request, headers['content-type'], maxBodySize, preserveRawBody);
|
|
226
|
+
body = bodyResult.body;
|
|
227
|
+
rawBody = bodyResult.rawBody;
|
|
228
|
+
}
|
|
229
|
+
const frameworkRequest = {
|
|
230
|
+
body,
|
|
231
|
+
cookies: parseCookieHeader(Array.isArray(headers.cookie) ? headers.cookie[0] : headers.cookie),
|
|
232
|
+
headers,
|
|
233
|
+
method: request.method,
|
|
234
|
+
params: {},
|
|
235
|
+
path: url.pathname,
|
|
236
|
+
query: parseQueryParams(url.searchParams),
|
|
237
|
+
raw: request,
|
|
238
|
+
signal,
|
|
239
|
+
url: url.pathname + url.search
|
|
240
|
+
};
|
|
241
|
+
if (files) {
|
|
242
|
+
frameworkRequest.files = files;
|
|
243
|
+
}
|
|
244
|
+
if (rawBody) {
|
|
245
|
+
frameworkRequest.rawBody = rawBody;
|
|
246
|
+
}
|
|
247
|
+
return frameworkRequest;
|
|
248
|
+
}
|
|
249
|
+
async function parseMultipartRequest(request, options = {}) {
|
|
250
|
+
try {
|
|
251
|
+
return await parseMultipart({
|
|
252
|
+
body: Readable.toWeb(request),
|
|
253
|
+
headers: normalizeHeaders(request.headers),
|
|
254
|
+
method: request.method,
|
|
255
|
+
url: new URL(request.url ?? '/', 'http://localhost').toString()
|
|
256
|
+
}, options);
|
|
257
|
+
} catch (error) {
|
|
258
|
+
if (isExpressMultipartTooLargeError(error)) {
|
|
259
|
+
if (error instanceof PayloadTooLargeException) {
|
|
260
|
+
throw error;
|
|
261
|
+
}
|
|
262
|
+
throw new PayloadTooLargeException('Request body exceeds the configured multipart limits.');
|
|
263
|
+
}
|
|
264
|
+
throw error;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
export function isExpressMultipartTooLargeError(error) {
|
|
268
|
+
if (error instanceof PayloadTooLargeException) {
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
if (!(error instanceof Error)) {
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
const candidate = error;
|
|
275
|
+
if (candidate.statusCode === 413 || candidate.status === 413) {
|
|
276
|
+
return true;
|
|
277
|
+
}
|
|
278
|
+
if (typeof candidate.code === 'string' && /LIMIT|TOO_LARGE|ENTITY_TOO_LARGE|FILE_TOO_LARGE/i.test(candidate.code)) {
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
if (typeof candidate.type === 'string' && candidate.type.toLowerCase() === 'entity.too.large') {
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
return error.message.toLowerCase().includes('too large');
|
|
285
|
+
}
|
|
286
|
+
function normalizeHeaders(headers) {
|
|
287
|
+
const normalized = {};
|
|
288
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
289
|
+
if (Array.isArray(value)) {
|
|
290
|
+
normalized[name] = value;
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
if (typeof value === 'number') {
|
|
294
|
+
normalized[name] = String(value);
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
if (typeof value === 'string' || value === undefined) {
|
|
298
|
+
normalized[name] = value;
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
normalized[name] = String(value);
|
|
302
|
+
}
|
|
303
|
+
return normalized;
|
|
304
|
+
}
|
|
305
|
+
function parseQueryParams(searchParams) {
|
|
306
|
+
const query = {};
|
|
307
|
+
for (const [key, value] of searchParams.entries()) {
|
|
308
|
+
const current = query[key];
|
|
309
|
+
if (current === undefined) {
|
|
310
|
+
query[key] = value;
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
if (Array.isArray(current)) {
|
|
314
|
+
current.push(value);
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
query[key] = [current, value];
|
|
318
|
+
}
|
|
319
|
+
return query;
|
|
320
|
+
}
|
|
321
|
+
function parseCookieHeader(cookieHeader) {
|
|
322
|
+
if (!cookieHeader) {
|
|
323
|
+
return {};
|
|
324
|
+
}
|
|
325
|
+
return Object.fromEntries(cookieHeader.split(';').map(pair => pair.trim()).filter(Boolean).map(pair => {
|
|
326
|
+
const index = pair.indexOf('=');
|
|
327
|
+
if (index === -1) {
|
|
328
|
+
return [pair.trim(), ''];
|
|
329
|
+
}
|
|
330
|
+
const rawValue = pair.slice(index + 1).trim();
|
|
331
|
+
try {
|
|
332
|
+
return [pair.slice(0, index).trim(), decodeURIComponent(rawValue)];
|
|
333
|
+
} catch {
|
|
334
|
+
return [pair.slice(0, index).trim(), rawValue];
|
|
335
|
+
}
|
|
336
|
+
}));
|
|
337
|
+
}
|
|
338
|
+
function createRequestSignal(response) {
|
|
339
|
+
const controller = new AbortController();
|
|
340
|
+
const abort = reason => {
|
|
341
|
+
if (!controller.signal.aborted) {
|
|
342
|
+
controller.abort(new Error(reason));
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
response.once('close', () => {
|
|
346
|
+
if (!response.writableEnded) {
|
|
347
|
+
abort('Response closed before response commit.');
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
return controller.signal;
|
|
351
|
+
}
|
|
352
|
+
function resolveRequestIdFromHeaders(headers) {
|
|
353
|
+
const requestId = headers['x-request-id'] ?? headers['x-correlation-id'];
|
|
354
|
+
return Array.isArray(requestId) ? requestId[0] : requestId;
|
|
355
|
+
}
|
|
356
|
+
function createExpressServer(httpsOptions, app) {
|
|
357
|
+
return httpsOptions ? createHttpsServer(httpsOptions, app) : createHttpServer(app);
|
|
358
|
+
}
|
|
359
|
+
function resolveListenTarget(address, port, host, useHttps) {
|
|
360
|
+
const protocol = useHttps ? 'https' : 'http';
|
|
361
|
+
const resolvedPort = typeof address === 'object' && address !== null ? address.port : port;
|
|
362
|
+
const bindHost = typeof address === 'object' && address !== null ? address.address : host ?? '0.0.0.0';
|
|
363
|
+
const publicHost = resolvePublicHost(host ?? bindHost);
|
|
364
|
+
const bindTarget = `${formatHostForAuthority(bindHost)}:${String(resolvedPort)}`;
|
|
365
|
+
const url = `${protocol}://${formatHostForAuthority(publicHost)}:${String(resolvedPort)}`;
|
|
366
|
+
return {
|
|
367
|
+
bindTarget,
|
|
368
|
+
url
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
function resolvePublicHost(host) {
|
|
372
|
+
return isWildcardHost(host) ? 'localhost' : host;
|
|
373
|
+
}
|
|
374
|
+
function isWildcardHost(host) {
|
|
375
|
+
return host === '0.0.0.0' || host === '::' || host === '[::]';
|
|
376
|
+
}
|
|
377
|
+
function formatHostForAuthority(host) {
|
|
378
|
+
return host.includes(':') && !host.startsWith('[') ? `[${host}]` : host;
|
|
379
|
+
}
|
|
380
|
+
function resolvePort(value) {
|
|
381
|
+
const port = value ?? 3000;
|
|
382
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535) {
|
|
383
|
+
throw new Error(`Invalid PORT value: ${String(value ?? 3000)}.`);
|
|
384
|
+
}
|
|
385
|
+
return port;
|
|
386
|
+
}
|
|
387
|
+
async function listenServer(server, port, host) {
|
|
388
|
+
await new Promise((resolve, reject) => {
|
|
389
|
+
const onError = error => {
|
|
390
|
+
cleanup();
|
|
391
|
+
reject(error);
|
|
392
|
+
};
|
|
393
|
+
const onListening = () => {
|
|
394
|
+
cleanup();
|
|
395
|
+
resolve();
|
|
396
|
+
};
|
|
397
|
+
const cleanup = () => {
|
|
398
|
+
server.off('error', onError);
|
|
399
|
+
server.off('listening', onListening);
|
|
400
|
+
};
|
|
401
|
+
server.once('error', onError);
|
|
402
|
+
server.once('listening', onListening);
|
|
403
|
+
try {
|
|
404
|
+
server.listen({
|
|
405
|
+
host,
|
|
406
|
+
port
|
|
407
|
+
});
|
|
408
|
+
} catch (error) {
|
|
409
|
+
cleanup();
|
|
410
|
+
reject(error);
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
function closeServerSilently(server) {
|
|
415
|
+
if (!server.listening) {
|
|
416
|
+
return Promise.resolve();
|
|
417
|
+
}
|
|
418
|
+
return new Promise(resolve => {
|
|
419
|
+
server.close(() => {
|
|
420
|
+
resolve();
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
function closeServerWithDrain(server, sockets, shutdownTimeoutMs) {
|
|
425
|
+
return new Promise((resolve, reject) => {
|
|
426
|
+
let settled = false;
|
|
427
|
+
const timeout = setTimeout(() => {
|
|
428
|
+
forceCloseConnections(server, sockets);
|
|
429
|
+
}, shutdownTimeoutMs);
|
|
430
|
+
const finish = error => {
|
|
431
|
+
if (settled) {
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
settled = true;
|
|
435
|
+
clearTimeout(timeout);
|
|
436
|
+
if (error) {
|
|
437
|
+
reject(error);
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
resolve();
|
|
441
|
+
};
|
|
442
|
+
server.close(error => {
|
|
443
|
+
finish(error);
|
|
444
|
+
});
|
|
445
|
+
closeIdleConnections(server);
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
function closeIdleConnections(server) {
|
|
449
|
+
server.closeIdleConnections?.();
|
|
450
|
+
}
|
|
451
|
+
function forceCloseConnections(server, sockets) {
|
|
452
|
+
if (typeof server.closeAllConnections === 'function') {
|
|
453
|
+
server.closeAllConnections();
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
for (const socket of sockets) {
|
|
457
|
+
socket.destroy();
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
function toHttpException(error) {
|
|
461
|
+
if (error instanceof HttpException) {
|
|
462
|
+
return error;
|
|
463
|
+
}
|
|
464
|
+
return new InternalServerErrorException('Internal server error.', {
|
|
465
|
+
cause: error
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
function isAddressInUseError(error) {
|
|
469
|
+
if (!(error instanceof Error)) {
|
|
470
|
+
return false;
|
|
471
|
+
}
|
|
472
|
+
return error.code === 'EADDRINUSE';
|
|
473
|
+
}
|
|
474
|
+
function delay(ms) {
|
|
475
|
+
return new Promise(resolve => {
|
|
476
|
+
setTimeout(resolve, ms);
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
function waitForCloseWithTimeout(closePromise, timeoutMs) {
|
|
480
|
+
return new Promise((resolve, reject) => {
|
|
481
|
+
const timeout = setTimeout(() => {
|
|
482
|
+
reject(new Error(`Express adapter shutdown timeout exceeded ${String(timeoutMs)}ms.`));
|
|
483
|
+
}, timeoutMs);
|
|
484
|
+
void closePromise.then(() => {
|
|
485
|
+
clearTimeout(timeout);
|
|
486
|
+
resolve();
|
|
487
|
+
}, error => {
|
|
488
|
+
clearTimeout(timeout);
|
|
489
|
+
reject(error);
|
|
490
|
+
});
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
function mergeSetCookieHeader(current, incoming) {
|
|
494
|
+
const nextValues = Array.isArray(incoming) ? incoming : [incoming];
|
|
495
|
+
if (current === undefined || typeof current === 'number') {
|
|
496
|
+
return nextValues.length === 1 ? nextValues[0] : [...nextValues];
|
|
497
|
+
}
|
|
498
|
+
const currentValues = Array.isArray(current) ? current : [current];
|
|
499
|
+
const merged = [...currentValues, ...nextValues];
|
|
500
|
+
return merged.length === 1 ? merged[0] : merged;
|
|
501
|
+
}
|
|
502
|
+
async function readRequestBody(request, contentType, maxBodySize = DEFAULT_MAX_BODY_SIZE, preserveRawBody = false) {
|
|
503
|
+
const chunks = [];
|
|
504
|
+
let totalSize = 0;
|
|
505
|
+
for await (const chunk of request) {
|
|
506
|
+
const bufferChunk = typeof chunk === 'string' ? Buffer.from(chunk) : chunk;
|
|
507
|
+
totalSize += bufferChunk.byteLength;
|
|
508
|
+
if (totalSize > maxBodySize) {
|
|
509
|
+
throw new PayloadTooLargeException('Request body exceeds the size limit.');
|
|
510
|
+
}
|
|
511
|
+
chunks.push(bufferChunk);
|
|
512
|
+
}
|
|
513
|
+
if (chunks.length === 0) {
|
|
514
|
+
return {
|
|
515
|
+
body: undefined
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
const rawBody = Buffer.concat(chunks);
|
|
519
|
+
const bodyText = rawBody.toString('utf8');
|
|
520
|
+
if (bodyText.length === 0) {
|
|
521
|
+
return {
|
|
522
|
+
body: undefined,
|
|
523
|
+
rawBody: preserveRawBody ? rawBody : undefined
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
const primaryContentType = readPrimaryHeaderValue(contentType);
|
|
527
|
+
if (typeof primaryContentType === 'string' && primaryContentType.includes('application/json')) {
|
|
528
|
+
try {
|
|
529
|
+
return {
|
|
530
|
+
body: JSON.parse(bodyText),
|
|
531
|
+
rawBody: preserveRawBody ? rawBody : undefined
|
|
532
|
+
};
|
|
533
|
+
} catch {
|
|
534
|
+
throw new BadRequestException('Request body contains invalid JSON.');
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
if (typeof primaryContentType === 'string' && primaryContentType.includes('application/x-www-form-urlencoded')) {
|
|
538
|
+
return {
|
|
539
|
+
body: parseUrlEncodedBody(bodyText),
|
|
540
|
+
rawBody: preserveRawBody ? rawBody : undefined
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
return {
|
|
544
|
+
body: bodyText,
|
|
545
|
+
rawBody: preserveRawBody ? rawBody : undefined
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
function parseUrlEncodedBody(bodyText) {
|
|
549
|
+
const fields = {};
|
|
550
|
+
const searchParams = new URLSearchParams(bodyText);
|
|
551
|
+
for (const [key, value] of searchParams.entries()) {
|
|
552
|
+
setMultiValue(fields, key, value);
|
|
553
|
+
}
|
|
554
|
+
return fields;
|
|
555
|
+
}
|
|
556
|
+
function readPrimaryHeaderValue(headerValue) {
|
|
557
|
+
if (Array.isArray(headerValue)) {
|
|
558
|
+
return headerValue[0];
|
|
559
|
+
}
|
|
560
|
+
return headerValue;
|
|
561
|
+
}
|
|
562
|
+
function setMultiValue(target, key, value) {
|
|
563
|
+
const existing = target[key];
|
|
564
|
+
if (existing === undefined) {
|
|
565
|
+
target[key] = value;
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
if (Array.isArray(existing)) {
|
|
569
|
+
existing.push(value);
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
target[key] = [existing, value];
|
|
573
|
+
}
|
|
574
|
+
function serializeResponseBody(body, contentType) {
|
|
575
|
+
if (body === undefined) {
|
|
576
|
+
return {
|
|
577
|
+
payload: ''
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
if (Buffer.isBuffer(body)) {
|
|
581
|
+
return {
|
|
582
|
+
defaultContentType: 'application/octet-stream',
|
|
583
|
+
payload: body
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
if (body instanceof Uint8Array) {
|
|
587
|
+
return {
|
|
588
|
+
defaultContentType: 'application/octet-stream',
|
|
589
|
+
payload: Buffer.from(body)
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
if (body instanceof ArrayBuffer) {
|
|
593
|
+
return {
|
|
594
|
+
defaultContentType: 'application/octet-stream',
|
|
595
|
+
payload: Buffer.from(body)
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
if (typeof body === 'string') {
|
|
599
|
+
const isJson = isJsonContentType(contentType);
|
|
600
|
+
return {
|
|
601
|
+
defaultContentType: isJson ? undefined : 'text/plain; charset=utf-8',
|
|
602
|
+
payload: isJson ? JSON.stringify(body) : body
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
return {
|
|
606
|
+
defaultContentType: 'application/json; charset=utf-8',
|
|
607
|
+
payload: JSON.stringify(body)
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
function isJsonContentType(contentType) {
|
|
611
|
+
return typeof contentType === 'string' && contentType.toLowerCase().includes('application/json');
|
|
612
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fluojs/platform-express",
|
|
3
|
+
"description": "Express-based HTTP adapter for the Fluo runtime.",
|
|
4
|
+
"keywords": [
|
|
5
|
+
"fluo",
|
|
6
|
+
"express",
|
|
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-express"
|
|
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
|
+
"express": "^5.1.0",
|
|
39
|
+
"@fluojs/http": "^1.0.0-beta.1",
|
|
40
|
+
"@fluojs/runtime": "^1.0.0-beta.1"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/express": "^5.0.3",
|
|
44
|
+
"vitest": "^3.2.4"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"prebuild": "node ../../tooling/scripts/clean-dist.mjs",
|
|
48
|
+
"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",
|
|
49
|
+
"typecheck": "pnpm exec tsc -p tsconfig.json --noEmit",
|
|
50
|
+
"test": "pnpm exec vitest run -c vitest.config.ts",
|
|
51
|
+
"test:watch": "pnpm exec vitest -c vitest.config.ts"
|
|
52
|
+
}
|
|
53
|
+
}
|