@dotpress/adapter-express 0.0.0-stage → 2.0.0-alpha.0
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.md +57 -2
- package/dist/index.d.ts +59 -0
- package/dist/index.js +373 -0
- package/dist/index.js.map +1 -0
- package/package.json +55 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-present Renaud Guyon
|
|
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.md
CHANGED
|
@@ -1,3 +1,58 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @dotpress/adapter-express
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[Express](https://expressjs.com) adapter for [Dotpress](https://dotpress.dev). Supports Express 4 and 5.
|
|
4
|
+
|
|
5
|
+
> 🚧 Dotpress V2 is under active development and not published yet.
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
pnpm add @dotpress/core @dotpress/adapter-express express zod
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { createApp, defineRoute } from '@dotpress/core'
|
|
13
|
+
import { expressAdapter } from '@dotpress/adapter-express'
|
|
14
|
+
import helmet from 'helmet'
|
|
15
|
+
|
|
16
|
+
defineRoute({ path: '/hello', handler: () => ({ message: 'Hello' }) })
|
|
17
|
+
|
|
18
|
+
const app = await createApp({
|
|
19
|
+
adapter: expressAdapter({
|
|
20
|
+
// Native Express middlewares, before the Dotpress routes
|
|
21
|
+
beforeRoutes: (app) => {
|
|
22
|
+
app.use(helmet())
|
|
23
|
+
},
|
|
24
|
+
}),
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
await app.listen(3000)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Options
|
|
31
|
+
|
|
32
|
+
| Option | Description |
|
|
33
|
+
| --- | --- |
|
|
34
|
+
| `app` | Existing Express application (Express 4 or 5). Defaults to a new `express()` application, with `X-Powered-By` and `ETag` disabled. |
|
|
35
|
+
| `beforeRoutes(app)` | Registers native middlewares before the Dotpress routes. |
|
|
36
|
+
| `afterRoutes(app)` | Registers native middlewares and routes after the Dotpress routes, before the not-found handler. |
|
|
37
|
+
| `bodyLimit` | Maximum request body size in bytes. Defaults to 1 MiB. |
|
|
38
|
+
|
|
39
|
+
## Behavior
|
|
40
|
+
|
|
41
|
+
- Routes are matched by Dotpress, so they behave the same on every adapter (case-sensitive, static segments before parameters, Express settings such as `case sensitive routing` do not apply).
|
|
42
|
+
- Request bodies are read by the adapter: JSON, URL-encoded forms and text are parsed, other content types are exposed as `Uint8Array`. Bodies already parsed by a native middleware (`express.json()`, `multer`…) are used as is.
|
|
43
|
+
- Routes declared with `rawBody: true` need the adapter to read the body: do not register a native body parser for them.
|
|
44
|
+
- Errors passed to `next(error)` by native middlewares use the Dotpress error format. Client errors (`status` 4xx) keep their status.
|
|
45
|
+
- `ctx.raw` is the Express request.
|
|
46
|
+
- `close()` waits for pending requests and closes the responses being streamed (Server-Sent Events).
|
|
47
|
+
- `adapter.server()` returns the Node.js HTTP server started by `listen()`.
|
|
48
|
+
|
|
49
|
+
To use your own HTTP server, mount the Express application yourself:
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
const app = await createApp({ adapter: expressAdapter() })
|
|
53
|
+
https.createServer(tlsOptions, app.native()).listen(443)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## License
|
|
57
|
+
|
|
58
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { IncomingMessage, ServerResponse, Server } from 'node:http';
|
|
2
|
+
import { HttpAdapter } from '@dotpress/core';
|
|
3
|
+
import { Express } from 'express';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Minimal shape of an Express application, compatible with Express 4 and 5.
|
|
7
|
+
*/
|
|
8
|
+
interface ExpressApplication {
|
|
9
|
+
(req: IncomingMessage, res: ServerResponse): void;
|
|
10
|
+
use(...handlers: unknown[]): unknown;
|
|
11
|
+
}
|
|
12
|
+
interface ExpressAdapterOptions<TApp extends ExpressApplication> {
|
|
13
|
+
/**
|
|
14
|
+
* Express application (Express 4 or 5) to mount the routes on. Defaults to
|
|
15
|
+
* a new application, created with `express()`.
|
|
16
|
+
*/
|
|
17
|
+
app?: TApp;
|
|
18
|
+
/** Registers native middlewares before the Dotpress routes (helmet…). */
|
|
19
|
+
beforeRoutes?: (app: TApp) => void | Promise<void>;
|
|
20
|
+
/**
|
|
21
|
+
* Registers native middlewares and routes after the Dotpress routes,
|
|
22
|
+
* before the not-found handler.
|
|
23
|
+
*/
|
|
24
|
+
afterRoutes?: (app: TApp) => void | Promise<void>;
|
|
25
|
+
/**
|
|
26
|
+
* Maximum request body size, in bytes. Defaults to 1 MiB. Routes can
|
|
27
|
+
* override it with their own `bodyLimit`.
|
|
28
|
+
*/
|
|
29
|
+
bodyLimit?: number;
|
|
30
|
+
}
|
|
31
|
+
interface ExpressAdapter<TApp extends ExpressApplication = Express> extends HttpAdapter<TApp> {
|
|
32
|
+
/** Starts an HTTP server. */
|
|
33
|
+
listen(port: number, host?: string): Promise<void>;
|
|
34
|
+
/**
|
|
35
|
+
* Stops the HTTP server: waits for pending requests, and closes the
|
|
36
|
+
* responses being streamed (their `req.signal` is aborted).
|
|
37
|
+
*/
|
|
38
|
+
close(): Promise<void>;
|
|
39
|
+
/** HTTP server started by `listen()`, if any. */
|
|
40
|
+
server(): Server | undefined;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Creates the Express adapter (Express 4 or 5).
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* const app = await createApp({
|
|
47
|
+
* adapter: expressAdapter({
|
|
48
|
+
* beforeRoutes: (app) => app.use(helmet()),
|
|
49
|
+
* }),
|
|
50
|
+
* })
|
|
51
|
+
* await app.listen(3000)
|
|
52
|
+
*/
|
|
53
|
+
declare function expressAdapter(options?: ExpressAdapterOptions<Express>): ExpressAdapter<Express>;
|
|
54
|
+
declare function expressAdapter<TApp extends ExpressApplication>(options: ExpressAdapterOptions<TApp> & {
|
|
55
|
+
app: TApp;
|
|
56
|
+
}): ExpressAdapter<TApp>;
|
|
57
|
+
|
|
58
|
+
export { expressAdapter };
|
|
59
|
+
export type { ExpressAdapter, ExpressAdapterOptions, ExpressApplication };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import {
|
|
3
|
+
createServer
|
|
4
|
+
} from "node:http";
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_BODY_LIMIT,
|
|
7
|
+
HttpError,
|
|
8
|
+
createRouteMatcher,
|
|
9
|
+
isHttpError,
|
|
10
|
+
malformedBodyError,
|
|
11
|
+
parseQueryString,
|
|
12
|
+
parseRequestBody,
|
|
13
|
+
payloadTooLargeError,
|
|
14
|
+
unsupportedMediaTypeError
|
|
15
|
+
} from "@dotpress/core";
|
|
16
|
+
import express from "express";
|
|
17
|
+
var trackConnections = (server) => {
|
|
18
|
+
const sockets = /* @__PURE__ */ new Set();
|
|
19
|
+
let closing = false;
|
|
20
|
+
server.on("connection", (socket) => {
|
|
21
|
+
sockets.add(socket);
|
|
22
|
+
socket.once("close", () => sockets.delete(socket));
|
|
23
|
+
});
|
|
24
|
+
server.on(
|
|
25
|
+
"request",
|
|
26
|
+
(_request, response) => {
|
|
27
|
+
response.once("finish", () => {
|
|
28
|
+
if (closing) server.closeIdleConnections();
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
);
|
|
32
|
+
return () => {
|
|
33
|
+
closing = true;
|
|
34
|
+
for (const socket of sockets) {
|
|
35
|
+
if (socket.bytesRead === 0) socket.destroy();
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
var CLIENT_GONE = /* @__PURE__ */ Symbol("client gone");
|
|
40
|
+
var pathnameOf = (url) => {
|
|
41
|
+
const index = url.indexOf("?");
|
|
42
|
+
return index === -1 ? url : url.slice(0, index);
|
|
43
|
+
};
|
|
44
|
+
var searchOf = (url) => {
|
|
45
|
+
const index = url.indexOf("?");
|
|
46
|
+
return index === -1 ? "" : url.slice(index);
|
|
47
|
+
};
|
|
48
|
+
var fullUrl = (req) => {
|
|
49
|
+
const host = req.headers.host;
|
|
50
|
+
if (!host) return void 0;
|
|
51
|
+
const protocol = req.protocol ?? "http";
|
|
52
|
+
return `${protocol}://${host}${req.originalUrl ?? req.url ?? "/"}`;
|
|
53
|
+
};
|
|
54
|
+
var hasBody = (req) => req.headers["transfer-encoding"] !== void 0 || req.headers["content-length"] !== void 0 && req.headers["content-length"] !== "0";
|
|
55
|
+
var discard = (req) => {
|
|
56
|
+
req.resume();
|
|
57
|
+
};
|
|
58
|
+
var NO_BODY = {
|
|
59
|
+
body: void 0,
|
|
60
|
+
files: void 0,
|
|
61
|
+
rawBody: void 0
|
|
62
|
+
};
|
|
63
|
+
var readBody = (req, limit) => {
|
|
64
|
+
if (req._body === true || req.readableEnded) {
|
|
65
|
+
return Promise.resolve({
|
|
66
|
+
body: req.body,
|
|
67
|
+
files: void 0,
|
|
68
|
+
rawBody: void 0
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
if (!hasBody(req)) {
|
|
72
|
+
return Promise.resolve(NO_BODY);
|
|
73
|
+
}
|
|
74
|
+
const encoding = (req.headers["content-encoding"] ?? "identity").trim().toLowerCase();
|
|
75
|
+
if (encoding !== "identity") {
|
|
76
|
+
discard(req);
|
|
77
|
+
return Promise.reject(
|
|
78
|
+
unsupportedMediaTypeError(`Unsupported content encoding "${encoding}"`)
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
const declaredLength = Number(req.headers["content-length"]);
|
|
82
|
+
if (Number.isFinite(declaredLength) && declaredLength > limit) {
|
|
83
|
+
discard(req);
|
|
84
|
+
return Promise.reject(payloadTooLargeError());
|
|
85
|
+
}
|
|
86
|
+
return new Promise((resolve, reject) => {
|
|
87
|
+
const chunks = [];
|
|
88
|
+
let size = 0;
|
|
89
|
+
const cleanup = () => {
|
|
90
|
+
req.off("data", onData);
|
|
91
|
+
req.off("end", onEnd);
|
|
92
|
+
req.off("error", onGone);
|
|
93
|
+
req.off("close", onClose);
|
|
94
|
+
};
|
|
95
|
+
function onData(chunk) {
|
|
96
|
+
size += chunk.byteLength;
|
|
97
|
+
if (size > limit) {
|
|
98
|
+
cleanup();
|
|
99
|
+
discard(req);
|
|
100
|
+
reject(payloadTooLargeError());
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
chunks.push(chunk);
|
|
104
|
+
}
|
|
105
|
+
function onEnd() {
|
|
106
|
+
cleanup();
|
|
107
|
+
const buffer = Buffer.concat(chunks, size);
|
|
108
|
+
const bytes = new Uint8Array(
|
|
109
|
+
buffer.buffer,
|
|
110
|
+
buffer.byteOffset,
|
|
111
|
+
buffer.byteLength
|
|
112
|
+
);
|
|
113
|
+
parseRequestBody(bytes, req.headers["content-type"]).then(
|
|
114
|
+
({ body, files }) => resolve({ body, files, rawBody: bytes }),
|
|
115
|
+
reject
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
function onGone() {
|
|
119
|
+
cleanup();
|
|
120
|
+
reject(CLIENT_GONE);
|
|
121
|
+
}
|
|
122
|
+
function onClose() {
|
|
123
|
+
if (!req.complete) onGone();
|
|
124
|
+
}
|
|
125
|
+
req.on("data", onData);
|
|
126
|
+
req.on("end", onEnd);
|
|
127
|
+
req.on("error", onGone);
|
|
128
|
+
req.on("close", onClose);
|
|
129
|
+
});
|
|
130
|
+
};
|
|
131
|
+
var ERROR_CODES = {
|
|
132
|
+
400: "BAD_REQUEST",
|
|
133
|
+
401: "UNAUTHORIZED",
|
|
134
|
+
403: "FORBIDDEN",
|
|
135
|
+
404: "NOT_FOUND",
|
|
136
|
+
405: "METHOD_NOT_ALLOWED",
|
|
137
|
+
408: "REQUEST_TIMEOUT",
|
|
138
|
+
409: "CONFLICT",
|
|
139
|
+
413: "PAYLOAD_TOO_LARGE",
|
|
140
|
+
415: "UNSUPPORTED_MEDIA_TYPE",
|
|
141
|
+
422: "UNPROCESSABLE_ENTITY",
|
|
142
|
+
429: "TOO_MANY_REQUESTS"
|
|
143
|
+
};
|
|
144
|
+
var fromNativeError = (error) => {
|
|
145
|
+
if (isHttpError(error) || typeof error !== "object" || error === null) {
|
|
146
|
+
return error;
|
|
147
|
+
}
|
|
148
|
+
const { type, status, statusCode, expose, message } = error;
|
|
149
|
+
if (type === "entity.parse.failed") {
|
|
150
|
+
return malformedBodyError();
|
|
151
|
+
}
|
|
152
|
+
const code = typeof status === "number" ? status : statusCode;
|
|
153
|
+
if (typeof code !== "number" || !Number.isInteger(code)) return error;
|
|
154
|
+
if (code < 400 || code > 499) return error;
|
|
155
|
+
return new HttpError(
|
|
156
|
+
code,
|
|
157
|
+
ERROR_CODES[code] ?? "CLIENT_ERROR",
|
|
158
|
+
expose === true && typeof message === "string" && message ? message : "Bad Request",
|
|
159
|
+
void 0,
|
|
160
|
+
{ cause: error }
|
|
161
|
+
);
|
|
162
|
+
};
|
|
163
|
+
var waitForDrain = (res) => new Promise((resolve) => {
|
|
164
|
+
const done = () => {
|
|
165
|
+
res.off("drain", done);
|
|
166
|
+
res.off("close", done);
|
|
167
|
+
resolve();
|
|
168
|
+
};
|
|
169
|
+
res.on("drain", done);
|
|
170
|
+
res.on("close", done);
|
|
171
|
+
});
|
|
172
|
+
function expressAdapter(options = {}) {
|
|
173
|
+
const bodyLimit = options.bodyLimit ?? DEFAULT_BODY_LIMIT;
|
|
174
|
+
if (!Number.isInteger(bodyLimit) || bodyLimit < 0) {
|
|
175
|
+
throw new RangeError(
|
|
176
|
+
"Invalid bodyLimit: expected a non-negative integer (bytes)."
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
let app = options.app;
|
|
180
|
+
if (!app) {
|
|
181
|
+
const created = express();
|
|
182
|
+
created.disable("x-powered-by");
|
|
183
|
+
created.disable("etag");
|
|
184
|
+
app = created;
|
|
185
|
+
}
|
|
186
|
+
const application = app;
|
|
187
|
+
let mounted = false;
|
|
188
|
+
let server;
|
|
189
|
+
let closeConnections = () => {
|
|
190
|
+
};
|
|
191
|
+
const activeStreams = /* @__PURE__ */ new Set();
|
|
192
|
+
const send = async (req, res, response, controller) => {
|
|
193
|
+
const { body } = response;
|
|
194
|
+
if (res.headersSent || res.destroyed) {
|
|
195
|
+
if (body instanceof ReadableStream) await body.cancel().catch(() => {
|
|
196
|
+
});
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
res.statusCode = response.status;
|
|
200
|
+
for (const [name, value] of Object.entries(response.headers)) {
|
|
201
|
+
res.setHeader(name, value);
|
|
202
|
+
}
|
|
203
|
+
if (body === null) {
|
|
204
|
+
res.end();
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (typeof body === "string" || body instanceof Uint8Array) {
|
|
208
|
+
if (!res.hasHeader("content-length")) {
|
|
209
|
+
res.setHeader(
|
|
210
|
+
"content-length",
|
|
211
|
+
typeof body === "string" ? Buffer.byteLength(body) : body.byteLength
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
res.end(body);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (req.method === "HEAD") {
|
|
218
|
+
await body.cancel().catch(() => {
|
|
219
|
+
});
|
|
220
|
+
res.end();
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
await pipe(body, res, controller);
|
|
224
|
+
};
|
|
225
|
+
const pipe = async (body, res, controller) => {
|
|
226
|
+
const reader = body.getReader();
|
|
227
|
+
const cancel = () => {
|
|
228
|
+
reader.cancel().catch(() => {
|
|
229
|
+
});
|
|
230
|
+
};
|
|
231
|
+
const stop = () => {
|
|
232
|
+
controller?.abort();
|
|
233
|
+
cancel();
|
|
234
|
+
};
|
|
235
|
+
res.on("close", cancel);
|
|
236
|
+
activeStreams.add(stop);
|
|
237
|
+
res.flushHeaders();
|
|
238
|
+
try {
|
|
239
|
+
for (; ; ) {
|
|
240
|
+
const { done, value } = await reader.read();
|
|
241
|
+
if (done || res.destroyed) break;
|
|
242
|
+
const writable = res.write(value);
|
|
243
|
+
res.flush?.();
|
|
244
|
+
if (!writable) await waitForDrain(res);
|
|
245
|
+
}
|
|
246
|
+
if (res.destroyed) cancel();
|
|
247
|
+
else res.end();
|
|
248
|
+
} catch {
|
|
249
|
+
res.destroy();
|
|
250
|
+
} finally {
|
|
251
|
+
res.off("close", cancel);
|
|
252
|
+
activeStreams.delete(stop);
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
const normalize = (req, signal) => {
|
|
256
|
+
const url = req.url ?? "/";
|
|
257
|
+
return {
|
|
258
|
+
method: req.method ?? "GET",
|
|
259
|
+
path: pathnameOf(url),
|
|
260
|
+
url: fullUrl(req),
|
|
261
|
+
params: {},
|
|
262
|
+
query: parseQueryString(searchOf(url)),
|
|
263
|
+
headers: req.headers,
|
|
264
|
+
body: void 0,
|
|
265
|
+
signal,
|
|
266
|
+
ip: req.ip ?? req.socket.remoteAddress,
|
|
267
|
+
raw: req
|
|
268
|
+
};
|
|
269
|
+
};
|
|
270
|
+
const fail = (res, runtime) => (error) => {
|
|
271
|
+
runtime.logger.error(
|
|
272
|
+
error instanceof Error ? error : { error },
|
|
273
|
+
"Express adapter failed to send the response"
|
|
274
|
+
);
|
|
275
|
+
if (!res.headersSent) {
|
|
276
|
+
res.statusCode = 500;
|
|
277
|
+
res.end();
|
|
278
|
+
} else {
|
|
279
|
+
res.destroy();
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
const handleRoute = async (req, res, match, runtime) => {
|
|
283
|
+
const controller = new AbortController();
|
|
284
|
+
res.on("close", () => {
|
|
285
|
+
if (!res.writableFinished) controller.abort();
|
|
286
|
+
});
|
|
287
|
+
const request = normalize(req, controller.signal);
|
|
288
|
+
try {
|
|
289
|
+
const { body, files, rawBody } = await readBody(
|
|
290
|
+
req,
|
|
291
|
+
match.route.definition.bodyLimit ?? bodyLimit
|
|
292
|
+
);
|
|
293
|
+
request.body = body;
|
|
294
|
+
request.files = files;
|
|
295
|
+
request.rawBody = rawBody;
|
|
296
|
+
} catch (error) {
|
|
297
|
+
if (error === CLIENT_GONE) return;
|
|
298
|
+
await send(req, res, await runtime.handleError(error, request));
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
request.params = match.params;
|
|
302
|
+
const response = await match.route.execute(request);
|
|
303
|
+
await send(req, res, response, controller);
|
|
304
|
+
};
|
|
305
|
+
return {
|
|
306
|
+
name: "express",
|
|
307
|
+
mount: async (routes, runtime) => {
|
|
308
|
+
if (mounted) {
|
|
309
|
+
throw new Error("The Express adapter is already mounted.");
|
|
310
|
+
}
|
|
311
|
+
mounted = true;
|
|
312
|
+
const matcher = createRouteMatcher(routes);
|
|
313
|
+
await options.beforeRoutes?.(application);
|
|
314
|
+
application.use(
|
|
315
|
+
(req, res, next) => {
|
|
316
|
+
const match = matcher.match(
|
|
317
|
+
req.method ?? "GET",
|
|
318
|
+
pathnameOf(req.url ?? "/")
|
|
319
|
+
);
|
|
320
|
+
if (!match) {
|
|
321
|
+
next();
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
handleRoute(req, res, match, runtime).catch(fail(res, runtime));
|
|
325
|
+
}
|
|
326
|
+
);
|
|
327
|
+
await options.afterRoutes?.(application);
|
|
328
|
+
application.use((req, res) => {
|
|
329
|
+
runtime.handleNotFound(normalize(req)).then((response) => send(req, res, response)).catch(fail(res, runtime));
|
|
330
|
+
});
|
|
331
|
+
application.use(
|
|
332
|
+
(error, req, res, next) => {
|
|
333
|
+
if (res.headersSent) {
|
|
334
|
+
next(error);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
runtime.handleError(fromNativeError(error), normalize(req)).then((response) => send(req, res, response)).catch(fail(res, runtime));
|
|
338
|
+
}
|
|
339
|
+
);
|
|
340
|
+
},
|
|
341
|
+
listen: async (port, host) => {
|
|
342
|
+
if (server) {
|
|
343
|
+
throw new Error("The Express adapter is already listening.");
|
|
344
|
+
}
|
|
345
|
+
const created = createServer(application);
|
|
346
|
+
await new Promise((resolve, reject) => {
|
|
347
|
+
created.once("error", reject);
|
|
348
|
+
created.listen(port, host, () => {
|
|
349
|
+
created.off("error", reject);
|
|
350
|
+
resolve();
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
server = created;
|
|
354
|
+
closeConnections = trackConnections(created);
|
|
355
|
+
},
|
|
356
|
+
close: async () => {
|
|
357
|
+
const current = server;
|
|
358
|
+
if (!current) return;
|
|
359
|
+
server = void 0;
|
|
360
|
+
await new Promise((resolve, reject) => {
|
|
361
|
+
current.close((error) => error ? reject(error) : resolve());
|
|
362
|
+
closeConnections();
|
|
363
|
+
for (const stop of activeStreams) stop();
|
|
364
|
+
});
|
|
365
|
+
},
|
|
366
|
+
native: () => application,
|
|
367
|
+
server: () => server
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
export {
|
|
371
|
+
expressAdapter
|
|
372
|
+
};
|
|
373
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import {\n createServer,\n type IncomingMessage,\n type RequestListener,\n type Server,\n type ServerResponse,\n} from 'node:http'\nimport type { Socket } from 'node:net'\nimport {\n DEFAULT_BODY_LIMIT,\n HttpError,\n createRouteMatcher,\n isHttpError,\n malformedBodyError,\n parseQueryString,\n parseRequestBody,\n payloadTooLargeError,\n unsupportedMediaTypeError,\n type CompiledRoute,\n type DotpressRuntime,\n type HttpAdapter,\n type NormalizedRequest,\n type NormalizedResponse,\n type RouteMatch,\n type UploadedFiles,\n} from '@dotpress/core'\nimport express, { type Express } from 'express'\n\n/**\n * Lets `close()` complete as soon as the pending requests are served, and\n * returns the function to call on close. Before Node.js 26, `server.close()`\n * waits for the connections that have sent nothing yet (a browser\n * preconnect…) until `headersTimeout`; and, whatever the version, the\n * connections of the pending requests stay open once served, until\n * `keepAliveTimeout`.\n */\nconst trackConnections = (server: Server) => {\n const sockets = new Set<Socket>()\n let closing = false\n server.on('connection', (socket: Socket) => {\n sockets.add(socket)\n socket.once('close', () => sockets.delete(socket))\n })\n server.on(\n 'request',\n (_request: IncomingMessage, response: ServerResponse) => {\n response.once('finish', () => {\n if (closing) server.closeIdleConnections()\n })\n }\n )\n return () => {\n closing = true\n for (const socket of sockets) {\n if (socket.bytesRead === 0) socket.destroy()\n }\n }\n}\n\n/**\n * Minimal shape of an Express application, compatible with Express 4 and 5.\n */\nexport interface ExpressApplication {\n (req: IncomingMessage, res: ServerResponse): void\n use(...handlers: unknown[]): unknown\n}\n\nexport interface ExpressAdapterOptions<TApp extends ExpressApplication> {\n /**\n * Express application (Express 4 or 5) to mount the routes on. Defaults to\n * a new application, created with `express()`.\n */\n app?: TApp\n /** Registers native middlewares before the Dotpress routes (helmet…). */\n beforeRoutes?: (app: TApp) => void | Promise<void>\n /**\n * Registers native middlewares and routes after the Dotpress routes,\n * before the not-found handler.\n */\n afterRoutes?: (app: TApp) => void | Promise<void>\n /**\n * Maximum request body size, in bytes. Defaults to 1 MiB. Routes can\n * override it with their own `bodyLimit`.\n */\n bodyLimit?: number\n}\n\nexport interface ExpressAdapter<\n TApp extends ExpressApplication = Express,\n> extends HttpAdapter<TApp> {\n /** Starts an HTTP server. */\n listen(port: number, host?: string): Promise<void>\n /**\n * Stops the HTTP server: waits for pending requests, and closes the\n * responses being streamed (their `req.signal` is aborted).\n */\n close(): Promise<void>\n /** HTTP server started by `listen()`, if any. */\n server(): Server | undefined\n}\n\n/** Express request fields used by the adapter (Express 4 and 5). */\ninterface ExpressRequest extends IncomingMessage {\n originalUrl?: string\n protocol?: string\n /** Client address, honoring the `trust proxy` setting. */\n ip?: string\n body?: unknown\n /** Set by body-parser and similar middlewares once the body is read. */\n _body?: boolean\n}\n\ntype NextFunction = (error?: unknown) => void\n\n/** Rejection reason when the client goes away while the body is read. */\nconst CLIENT_GONE = Symbol('client gone')\n\nconst pathnameOf = (url: string) => {\n const index = url.indexOf('?')\n return index === -1 ? url : url.slice(0, index)\n}\n\nconst searchOf = (url: string) => {\n const index = url.indexOf('?')\n return index === -1 ? '' : url.slice(index)\n}\n\nconst fullUrl = (req: ExpressRequest): string | undefined => {\n const host = req.headers.host\n if (!host) return undefined\n const protocol = req.protocol ?? 'http'\n return `${protocol}://${host}${req.originalUrl ?? req.url ?? '/'}`\n}\n\nconst hasBody = (req: IncomingMessage) =>\n req.headers['transfer-encoding'] !== undefined ||\n (req.headers['content-length'] !== undefined &&\n req.headers['content-length'] !== '0')\n\n/** Discards the rest of the request body. */\nconst discard = (req: IncomingMessage) => {\n req.resume()\n}\n\ninterface RequestBody {\n readonly body: unknown\n readonly files: UploadedFiles | undefined\n /** Undefined when a native middleware read the body. */\n readonly rawBody: Uint8Array | undefined\n}\n\nconst NO_BODY: RequestBody = {\n body: undefined,\n files: undefined,\n rawBody: undefined,\n}\n\nconst readBody = (req: ExpressRequest, limit: number): Promise<RequestBody> => {\n // The body was already read by a native middleware (express.json(),\n // multer…): use its result.\n if (req._body === true || req.readableEnded) {\n return Promise.resolve({\n body: req.body,\n files: undefined,\n rawBody: undefined,\n })\n }\n\n if (!hasBody(req)) {\n return Promise.resolve(NO_BODY)\n }\n\n const encoding = (req.headers['content-encoding'] ?? 'identity')\n .trim()\n .toLowerCase()\n\n if (encoding !== 'identity') {\n discard(req)\n return Promise.reject(\n unsupportedMediaTypeError(`Unsupported content encoding \"${encoding}\"`)\n )\n }\n\n const declaredLength = Number(req.headers['content-length'])\n if (Number.isFinite(declaredLength) && declaredLength > limit) {\n discard(req)\n return Promise.reject(payloadTooLargeError())\n }\n\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = []\n let size = 0\n\n const cleanup = () => {\n req.off('data', onData)\n req.off('end', onEnd)\n req.off('error', onGone)\n req.off('close', onClose)\n }\n\n function onData(chunk: Buffer) {\n size += chunk.byteLength\n if (size > limit) {\n cleanup()\n discard(req)\n reject(payloadTooLargeError())\n return\n }\n chunks.push(chunk)\n }\n\n function onEnd() {\n cleanup()\n const buffer = Buffer.concat(chunks, size)\n const bytes = new Uint8Array(\n buffer.buffer,\n buffer.byteOffset,\n buffer.byteLength\n )\n parseRequestBody(bytes, req.headers['content-type']).then(\n ({ body, files }) => resolve({ body, files, rawBody: bytes }),\n reject\n )\n }\n\n function onGone() {\n cleanup()\n reject(CLIENT_GONE)\n }\n\n function onClose() {\n if (!req.complete) onGone()\n }\n\n req.on('data', onData)\n req.on('end', onEnd)\n req.on('error', onGone)\n req.on('close', onClose)\n })\n}\n\nconst ERROR_CODES: Record<number, string> = {\n 400: 'BAD_REQUEST',\n 401: 'UNAUTHORIZED',\n 403: 'FORBIDDEN',\n 404: 'NOT_FOUND',\n 405: 'METHOD_NOT_ALLOWED',\n 408: 'REQUEST_TIMEOUT',\n 409: 'CONFLICT',\n 413: 'PAYLOAD_TOO_LARGE',\n 415: 'UNSUPPORTED_MEDIA_TYPE',\n 422: 'UNPROCESSABLE_ENTITY',\n 429: 'TOO_MANY_REQUESTS',\n}\n\n/**\n * Converts errors raised by native middlewares (`next(error)`) into Dotpress\n * errors. Client errors (4xx) keep their status; anything else is an\n * unexpected error.\n */\nconst fromNativeError = (error: unknown): unknown => {\n if (isHttpError(error) || typeof error !== 'object' || error === null) {\n return error\n }\n\n const { type, status, statusCode, expose, message } = error as {\n type?: unknown\n status?: unknown\n statusCode?: unknown\n expose?: unknown\n message?: unknown\n }\n\n if (type === 'entity.parse.failed') {\n return malformedBodyError()\n }\n\n const code = typeof status === 'number' ? status : statusCode\n if (typeof code !== 'number' || !Number.isInteger(code)) return error\n if (code < 400 || code > 499) return error\n\n return new HttpError(\n code,\n ERROR_CODES[code] ?? 'CLIENT_ERROR',\n expose === true && typeof message === 'string' && message\n ? message\n : 'Bad Request',\n undefined,\n { cause: error }\n )\n}\n\nconst waitForDrain = (res: ServerResponse) =>\n new Promise<void>((resolve) => {\n const done = () => {\n res.off('drain', done)\n res.off('close', done)\n resolve()\n }\n res.on('drain', done)\n res.on('close', done)\n })\n\n/**\n * Creates the Express adapter (Express 4 or 5).\n *\n * @example\n * const app = await createApp({\n * adapter: expressAdapter({\n * beforeRoutes: (app) => app.use(helmet()),\n * }),\n * })\n * await app.listen(3000)\n */\nexport function expressAdapter(\n options?: ExpressAdapterOptions<Express>\n): ExpressAdapter<Express>\nexport function expressAdapter<TApp extends ExpressApplication>(\n options: ExpressAdapterOptions<TApp> & { app: TApp }\n): ExpressAdapter<TApp>\nexport function expressAdapter<TApp extends ExpressApplication>(\n options: ExpressAdapterOptions<TApp> = {}\n): ExpressAdapter<TApp> {\n const bodyLimit = options.bodyLimit ?? DEFAULT_BODY_LIMIT\n\n if (!Number.isInteger(bodyLimit) || bodyLimit < 0) {\n throw new RangeError(\n 'Invalid bodyLimit: expected a non-negative integer (bytes).'\n )\n }\n\n let app = options.app\n if (!app) {\n const created = express()\n created.disable('x-powered-by')\n created.disable('etag')\n app = created as unknown as TApp\n }\n\n const application = app\n let mounted = false\n let server: Server | undefined\n let closeConnections = () => {}\n /** Aborts the responses being streamed, so that `close()` can complete. */\n const activeStreams = new Set<() => void>()\n\n const send = async (\n req: IncomingMessage,\n res: ServerResponse,\n response: NormalizedResponse,\n controller?: AbortController\n ): Promise<void> => {\n const { body } = response\n\n if (res.headersSent || res.destroyed) {\n if (body instanceof ReadableStream) await body.cancel().catch(() => {})\n return\n }\n\n res.statusCode = response.status\n for (const [name, value] of Object.entries(response.headers)) {\n res.setHeader(name, value)\n }\n\n if (body === null) {\n res.end()\n return\n }\n\n if (typeof body === 'string' || body instanceof Uint8Array) {\n if (!res.hasHeader('content-length')) {\n res.setHeader(\n 'content-length',\n typeof body === 'string' ? Buffer.byteLength(body) : body.byteLength\n )\n }\n res.end(body)\n return\n }\n\n if (req.method === 'HEAD') {\n await body.cancel().catch(() => {})\n res.end()\n return\n }\n\n await pipe(body, res, controller)\n }\n\n const pipe = async (\n body: ReadableStream<Uint8Array>,\n res: ServerResponse,\n controller: AbortController | undefined\n ) => {\n const reader = body.getReader()\n const cancel = () => {\n reader.cancel().catch(() => {})\n }\n const stop = () => {\n controller?.abort()\n cancel()\n }\n\n res.on('close', cancel)\n activeStreams.add(stop)\n // Sends the headers right away (Server-Sent Events).\n res.flushHeaders()\n\n try {\n for (;;) {\n const { done, value } = await reader.read()\n if (done || res.destroyed) break\n\n const writable = res.write(value)\n // Flushes compression middlewares (`compression` adds `res.flush`).\n ;(res as { flush?: () => void }).flush?.()\n if (!writable) await waitForDrain(res)\n }\n\n if (res.destroyed) cancel()\n else res.end()\n } catch {\n // Ending the response would hide the failure: abort the connection.\n res.destroy()\n } finally {\n res.off('close', cancel)\n activeStreams.delete(stop)\n }\n }\n\n const normalize = (\n req: ExpressRequest,\n signal?: AbortSignal\n ): NormalizedRequest => {\n const url = req.url ?? '/'\n return {\n method: req.method ?? 'GET',\n path: pathnameOf(url),\n url: fullUrl(req),\n params: {},\n query: parseQueryString(searchOf(url)),\n headers: req.headers,\n body: undefined,\n signal,\n ip: req.ip ?? req.socket.remoteAddress,\n raw: req,\n }\n }\n\n const fail =\n (res: ServerResponse, runtime: DotpressRuntime) => (error: unknown) => {\n runtime.logger.error(\n error instanceof Error ? error : { error },\n 'Express adapter failed to send the response'\n )\n if (!res.headersSent) {\n res.statusCode = 500\n res.end()\n } else {\n res.destroy()\n }\n }\n\n const handleRoute = async (\n req: ExpressRequest,\n res: ServerResponse,\n match: RouteMatch,\n runtime: DotpressRuntime\n ) => {\n const controller = new AbortController()\n res.on('close', () => {\n if (!res.writableFinished) controller.abort()\n })\n\n const request = normalize(req, controller.signal)\n\n try {\n const { body, files, rawBody } = await readBody(\n req,\n match.route.definition.bodyLimit ?? bodyLimit\n )\n request.body = body\n request.files = files\n request.rawBody = rawBody\n } catch (error) {\n if (error === CLIENT_GONE) return\n await send(req, res, await runtime.handleError(error, request))\n return\n }\n\n request.params = match.params\n const response = await match.route.execute(request)\n await send(req, res, response, controller)\n }\n\n return {\n name: 'express',\n\n mount: async (\n routes: readonly CompiledRoute[],\n runtime: DotpressRuntime\n ) => {\n if (mounted) {\n throw new Error('The Express adapter is already mounted.')\n }\n mounted = true\n\n const matcher = createRouteMatcher(routes)\n\n await options.beforeRoutes?.(application)\n\n application.use(\n (req: ExpressRequest, res: ServerResponse, next: NextFunction) => {\n const match = matcher.match(\n req.method ?? 'GET',\n pathnameOf(req.url ?? '/')\n )\n if (!match) {\n next()\n return\n }\n handleRoute(req, res, match, runtime).catch(fail(res, runtime))\n }\n )\n\n await options.afterRoutes?.(application)\n\n application.use((req: ExpressRequest, res: ServerResponse) => {\n runtime\n .handleNotFound(normalize(req))\n .then((response) => send(req, res, response))\n .catch(fail(res, runtime))\n })\n\n application.use(\n (\n error: unknown,\n req: ExpressRequest,\n res: ServerResponse,\n next: NextFunction\n ) => {\n if (res.headersSent) {\n next(error)\n return\n }\n runtime\n .handleError(fromNativeError(error), normalize(req))\n .then((response) => send(req, res, response))\n .catch(fail(res, runtime))\n }\n )\n },\n\n listen: async (port: number, host?: string) => {\n if (server) {\n throw new Error('The Express adapter is already listening.')\n }\n\n const created = createServer(application as RequestListener)\n await new Promise<void>((resolve, reject) => {\n created.once('error', reject)\n created.listen(port, host, () => {\n created.off('error', reject)\n resolve()\n })\n })\n server = created\n closeConnections = trackConnections(created)\n },\n\n close: async () => {\n const current = server\n if (!current) return\n server = undefined\n\n await new Promise<void>((resolve, reject) => {\n current.close((error) => (error ? reject(error) : resolve()))\n closeConnections()\n // Streams never end by themselves: abort them.\n for (const stop of activeStreams) stop()\n })\n },\n\n native: () => application,\n server: () => server,\n }\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,58 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotpress/adapter-express",
|
|
3
|
-
"version": "0.0.0
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "2.0.0-alpha.0",
|
|
4
|
+
"description": "Express 4 and 5 adapter for Dotpress",
|
|
5
|
+
"author": "Renaud Guyon",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"homepage": "https://dotpress.dev",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/dotpressjs/dotpress.git",
|
|
11
|
+
"directory": "packages/adapter-express"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/dotpressjs/dotpress/issues"
|
|
15
|
+
},
|
|
16
|
+
"type": "module",
|
|
17
|
+
"sideEffects": false,
|
|
18
|
+
"files": [
|
|
19
|
+
"dist"
|
|
20
|
+
],
|
|
21
|
+
"main": "./dist/index.js",
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"default": "./dist/index.js"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=22.12"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"@dotpress/core": "^2.0.0-alpha.0",
|
|
34
|
+
"express": "^4.21.0 || ^5.0.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@dotpress/core": "2.0.0-alpha.0",
|
|
38
|
+
"@dotpress/testing": "2.0.0-alpha.0",
|
|
39
|
+
"@types/express": "^5.0.6",
|
|
40
|
+
"express": "^5.3.0",
|
|
41
|
+
"express-4": "npm:express@^4.22.3",
|
|
42
|
+
"zod": "^4.6.7"
|
|
43
|
+
},
|
|
44
|
+
"keywords": [
|
|
45
|
+
"dotpress",
|
|
46
|
+
"express",
|
|
47
|
+
"adapter",
|
|
48
|
+
"typescript",
|
|
49
|
+
"zod"
|
|
50
|
+
],
|
|
51
|
+
"scripts": {
|
|
52
|
+
"build": "tsup",
|
|
53
|
+
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json",
|
|
54
|
+
"lint": "eslint src test",
|
|
55
|
+
"test": "vitest run",
|
|
56
|
+
"test:watch": "vitest watch"
|
|
57
|
+
}
|
|
6
58
|
}
|