remix 3.0.0-beta.0 → 3.0.0-beta.2
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/dist/fetch-router.d.ts +7 -0
- package/dist/fetch-router.d.ts.map +1 -1
- package/dist/node-tsx/load-module.d.ts +2 -0
- package/dist/node-tsx/load-module.d.ts.map +1 -0
- package/dist/node-tsx/load-module.js +2 -0
- package/dist/node-tsx.d.ts +3 -0
- package/dist/node-tsx.d.ts.map +1 -0
- package/{src/node-serve.ts → dist/node-tsx.js} +2 -1
- package/dist/render-middleware.d.ts +2 -0
- package/dist/render-middleware.d.ts.map +1 -0
- package/dist/render-middleware.js +2 -0
- package/dist/route-pattern/href.d.ts +2 -0
- package/dist/route-pattern/href.d.ts.map +1 -0
- package/dist/route-pattern/href.js +2 -0
- package/dist/route-pattern/join.d.ts +2 -0
- package/dist/route-pattern/join.d.ts.map +1 -0
- package/dist/route-pattern/join.js +2 -0
- package/dist/route-pattern/match.d.ts +2 -0
- package/dist/route-pattern/match.d.ts.map +1 -0
- package/dist/route-pattern/match.js +2 -0
- package/package.json +158 -44
- package/src/assert/README.md +109 -0
- package/src/assets/README.md +539 -0
- package/src/async-context-middleware/README.md +100 -0
- package/src/auth/README.md +445 -0
- package/src/auth-middleware/README.md +246 -0
- package/src/cli/README.md +78 -0
- package/src/compression-middleware/README.md +176 -0
- package/src/cookie/README.md +106 -0
- package/src/cop-middleware/README.md +117 -0
- package/src/cors-middleware/README.md +174 -0
- package/src/csrf-middleware/README.md +99 -0
- package/src/data-schema/README.md +422 -0
- package/src/data-table/README.md +552 -0
- package/src/data-table-mysql/README.md +97 -0
- package/src/data-table-postgres/README.md +74 -0
- package/src/data-table-sqlite/README.md +84 -0
- package/src/fetch-proxy/README.md +46 -0
- package/src/fetch-router/README.md +902 -0
- package/src/fetch-router.ts +7 -0
- package/src/file-storage/README.md +57 -0
- package/src/file-storage-s3/README.md +47 -0
- package/src/form-data-middleware/README.md +109 -0
- package/src/form-data-parser/README.md +160 -0
- package/src/fs/README.md +60 -0
- package/src/headers/README.md +629 -0
- package/src/html-template/README.md +101 -0
- package/src/lazy-file/README.md +109 -0
- package/src/logger-middleware/README.md +132 -0
- package/src/method-override-middleware/README.md +71 -0
- package/src/mime/README.md +110 -0
- package/src/multipart-parser/README.md +241 -0
- package/src/node-fetch-server/README.md +352 -0
- package/src/node-tsx/README.md +79 -0
- package/src/node-tsx/load-module.ts +2 -0
- package/{dist/node-serve.js → src/node-tsx.ts} +2 -1
- package/src/render-middleware/README.md +99 -0
- package/src/render-middleware.ts +2 -0
- package/src/route-pattern/README.md +291 -0
- package/src/route-pattern/href.ts +2 -0
- package/src/route-pattern/join.ts +2 -0
- package/src/route-pattern/match.ts +2 -0
- package/src/session/README.md +171 -0
- package/src/session-middleware/README.md +109 -0
- package/src/session-storage-memcache/README.md +37 -0
- package/src/session-storage-redis/README.md +37 -0
- package/src/static-middleware/README.md +89 -0
- package/src/tar-parser/README.md +74 -0
- package/src/terminal/README.md +92 -0
- package/src/test/README.md +430 -0
- package/src/ui/README.md +219 -0
- package/src/ui/accordion/README.md +166 -0
- package/src/ui/anchor/README.md +153 -0
- package/src/ui/animation/README.md +316 -0
- package/src/ui/breadcrumbs/README.md +55 -0
- package/src/ui/button/README.md +44 -0
- package/src/ui/combobox/README.md +145 -0
- package/src/ui/glyph/README.md +72 -0
- package/src/ui/listbox/README.md +115 -0
- package/src/ui/menu/README.md +96 -0
- package/src/ui/popover/README.md +122 -0
- package/src/ui/scroll-lock/README.md +33 -0
- package/src/ui/select/README.md +107 -0
- package/src/ui/server/README.md +90 -0
- package/src/ui/test/README.md +107 -0
- package/src/ui/theme/README.md +103 -0
- package/dist/node-serve.d.ts +0 -2
- package/dist/node-serve.d.ts.map +0 -1
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
# multipart-parser
|
|
2
|
+
|
|
3
|
+
Fast streaming multipart parsing for JavaScript. `multipart-parser` processes multipart bodies incrementally so large uploads can be handled without buffering the entire multipart payload in memory.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **File Upload Parsing** - Parse file uploads (`multipart/form-data`) with automatic field and file detection
|
|
8
|
+
- **Full Multipart Support** - Support for all `multipart/*` content types (mixed, alternative, related, etc.)
|
|
9
|
+
- **Convenient API** - `MultipartPart` API with `arrayBuffer`, `bytes`, `text`, `size`, and metadata access
|
|
10
|
+
- **Built-in Limits** - Header, per-part, part-count, and aggregate-size limits to prevent abuse
|
|
11
|
+
- **Node.js Support** - First-class Node.js support with native `http.IncomingMessage` compatibility
|
|
12
|
+
- **Runtime Demos** - [Demos for every major runtime](https://github.com/remix-run/remix/tree/main/packages/multipart-parser/demos)
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
npm i remix
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
The most common use case for `multipart-parser` is handling file uploads when you're building a web server. For this case, the `parseMultipartRequest` function is your friend. It automatically validates the request is `multipart/form-data`, extracts the multipart boundary from the `Content-Type` header, parses all fields and files in the `request.body` stream, and gives each one to you as a `MultipartPart` object with a rich API for accessing its metadata and content.
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { MultipartParseError, parseMultipartRequest } from 'remix/multipart-parser'
|
|
26
|
+
|
|
27
|
+
async function handleRequest(request: Request): void {
|
|
28
|
+
try {
|
|
29
|
+
for await (let part of parseMultipartRequest(request)) {
|
|
30
|
+
if (part.isFile) {
|
|
31
|
+
// Access file data in multiple formats
|
|
32
|
+
let buffer = part.arrayBuffer // ArrayBuffer
|
|
33
|
+
console.log(`File received: ${part.filename} (${buffer.byteLength} bytes)`)
|
|
34
|
+
console.log(`Content type: ${part.mediaType}`)
|
|
35
|
+
console.log(`Field name: ${part.name}`)
|
|
36
|
+
console.log(`Content-Type header: ${part.headers['content-type']}`)
|
|
37
|
+
|
|
38
|
+
// Save to disk, upload to cloud storage, etc.
|
|
39
|
+
await saveFile(part.filename, part.bytes)
|
|
40
|
+
} else {
|
|
41
|
+
let text = part.text // string
|
|
42
|
+
console.log(`Field received: ${part.name} = ${JSON.stringify(text)}`)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
} catch (error) {
|
|
46
|
+
if (error instanceof MultipartParseError) {
|
|
47
|
+
console.error('Failed to parse multipart request:', error.message)
|
|
48
|
+
} else {
|
|
49
|
+
console.error('An unexpected error occurred:', error)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Part Headers
|
|
56
|
+
|
|
57
|
+
Each `MultipartPart` exposes decoded part headers as a plain object keyed by lower-case header name. Values are strings, and repeated headers are joined with `, `. Multipart part headers are parsed metadata from the request body, not native `Headers` objects, so access them with bracket notation:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
for await (let part of parseMultipartRequest(request)) {
|
|
61
|
+
let contentDisposition = part.headers['content-disposition']
|
|
62
|
+
let contentType = part.headers['content-type']
|
|
63
|
+
|
|
64
|
+
console.log(contentDisposition, contentType)
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Size Limits
|
|
69
|
+
|
|
70
|
+
A common use case when handling file uploads is limiting the overall shape of incoming multipart bodies so malicious clients cannot force unbounded growth in memory. Use `maxFileSize` to limit each part, `maxParts` to limit how many parts are accepted, and `maxTotalSize` to limit aggregate part content across the entire request. `multipart-parser` applies finite defaults for each of these limits.
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
import {
|
|
74
|
+
MultipartParseError,
|
|
75
|
+
MaxFileSizeExceededError,
|
|
76
|
+
MaxPartsExceededError,
|
|
77
|
+
MaxTotalSizeExceededError,
|
|
78
|
+
parseMultipartRequest,
|
|
79
|
+
} from 'remix/multipart-parser/node'
|
|
80
|
+
|
|
81
|
+
const oneMb = Math.pow(2, 20)
|
|
82
|
+
const limits = {
|
|
83
|
+
maxFileSize: 10 * oneMb,
|
|
84
|
+
maxParts: 100,
|
|
85
|
+
maxTotalSize: 25 * oneMb,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function handleRequest(request: Request): Promise<Response> {
|
|
89
|
+
try {
|
|
90
|
+
for await (let part of parseMultipartRequest(request, limits)) {
|
|
91
|
+
// ...
|
|
92
|
+
}
|
|
93
|
+
} catch (error) {
|
|
94
|
+
if (error instanceof MaxFileSizeExceededError) {
|
|
95
|
+
return new Response('File size limit exceeded', { status: 413 })
|
|
96
|
+
} else if (error instanceof MaxPartsExceededError) {
|
|
97
|
+
return new Response('Too many multipart parts', { status: 413 })
|
|
98
|
+
} else if (error instanceof MaxTotalSizeExceededError) {
|
|
99
|
+
return new Response('Multipart request is too large', { status: 413 })
|
|
100
|
+
} else if (error instanceof MultipartParseError) {
|
|
101
|
+
return new Response('Failed to parse multipart request', { status: 400 })
|
|
102
|
+
} else {
|
|
103
|
+
console.error(error)
|
|
104
|
+
return new Response('Internal Server Error', { status: 500 })
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Node.js Bindings
|
|
111
|
+
|
|
112
|
+
The main module (`import {} from 'remix/multipart-parser'`) assumes you're working with [the fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) (`Request`, `ReadableStream`, etc). Support for these interfaces was added to Node.js by the [undici](https://github.com/nodejs/undici) project in [version 16.5.0](https://nodejs.org/en/blog/release/v16.5.0).
|
|
113
|
+
|
|
114
|
+
If however you're building a server for Node.js that relies on node-specific APIs like `http.IncomingMessage`, `stream.Readable`, and `buffer.Buffer` (ala Express or `http.createServer`), `multipart-parser` ships with an additional module that works directly with these APIs.
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
import * as http from 'node:http'
|
|
118
|
+
import { MultipartParseError, parseMultipartRequest } from 'remix/multipart-parser/node'
|
|
119
|
+
|
|
120
|
+
let server = http.createServer(async (req, res) => {
|
|
121
|
+
try {
|
|
122
|
+
for await (let part of parseMultipartRequest(req)) {
|
|
123
|
+
// ...
|
|
124
|
+
}
|
|
125
|
+
} catch (error) {
|
|
126
|
+
if (error instanceof MultipartParseError) {
|
|
127
|
+
console.error('Failed to parse multipart request:', error.message)
|
|
128
|
+
} else {
|
|
129
|
+
console.error('An unexpected error occurred:', error)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
server.listen(8080)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Low-level API
|
|
138
|
+
|
|
139
|
+
If you're working directly with multipart boundaries and buffers/streams of multipart data that are not necessarily part of a request, `multipart-parser` provides a low-level `parseMultipart()` API that you can use directly:
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
import { parseMultipart } from 'remix/multipart-parser'
|
|
143
|
+
|
|
144
|
+
let message = new Uint8Array(/* ... */)
|
|
145
|
+
let boundary = '----WebKitFormBoundary56eac3x'
|
|
146
|
+
|
|
147
|
+
for (let part of parseMultipart(message, { boundary })) {
|
|
148
|
+
// ...
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
In addition, the `parseMultipartStream` function provides an `async` generator interface for multipart data in a `ReadableStream`:
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
import { parseMultipartStream } from 'remix/multipart-parser'
|
|
156
|
+
|
|
157
|
+
let message = new ReadableStream(/* ... */)
|
|
158
|
+
let boundary = '----WebKitFormBoundary56eac3x'
|
|
159
|
+
|
|
160
|
+
for await (let part of parseMultipartStream(message, { boundary })) {
|
|
161
|
+
// ...
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Demos
|
|
166
|
+
|
|
167
|
+
The [`demos` directory](https://github.com/remix-run/remix/tree/main/packages/multipart-parser/demos) contains a few working demos of how you can use this library:
|
|
168
|
+
|
|
169
|
+
- [`demos/bun`](https://github.com/remix-run/remix/tree/main/packages/multipart-parser/demos/bun) - using multipart-parser in Bun
|
|
170
|
+
- [`demos/cf-workers`](https://github.com/remix-run/remix/tree/main/packages/multipart-parser/demos/cf-workers) - using multipart-parser in a Cloudflare Worker and storing file uploads in R2
|
|
171
|
+
- [`demos/deno`](https://github.com/remix-run/remix/tree/main/packages/multipart-parser/demos/deno) - using multipart-parser in Deno
|
|
172
|
+
- [`demos/node`](https://github.com/remix-run/remix/tree/main/packages/multipart-parser/demos/node) - using multipart-parser in Node.js
|
|
173
|
+
|
|
174
|
+
## Benchmark
|
|
175
|
+
|
|
176
|
+
`multipart-parser` is designed to be as efficient as possible, operating on streams of data and rarely buffering in common usage. This design yields exceptional performance when handling multipart payloads of any size. In benchmarks, `multipart-parser` is as fast or faster than `busboy`.
|
|
177
|
+
|
|
178
|
+
The results of running the benchmarks on my laptop:
|
|
179
|
+
|
|
180
|
+
```
|
|
181
|
+
> @remix-run/multipart-parser@0.10.1 bench:node /Users/michael/Projects/remix-the-web/packages/multipart-parser
|
|
182
|
+
> node ./bench/runner.ts
|
|
183
|
+
|
|
184
|
+
Platform: Darwin (24.5.0)
|
|
185
|
+
CPU: Apple M1 Pro
|
|
186
|
+
Date: 6/13/2025, 12:27:09 PM
|
|
187
|
+
Node.js v24.0.2
|
|
188
|
+
┌──────────────────┬──────────────────┬──────────────────┬──────────────────┬───────────────────┐
|
|
189
|
+
│ (index) │ 1 small file │ 1 large file │ 100 small files │ 5 large files │
|
|
190
|
+
├──────────────────┼──────────────────┼──────────────────┼──────────────────┼───────────────────┤
|
|
191
|
+
│ multipart-parser │ '0.01 ms ± 0.03' │ '1.08 ms ± 0.08' │ '0.04 ms ± 0.01' │ '10.50 ms ± 0.38' │
|
|
192
|
+
│ multipasta │ '0.02 ms ± 0.06' │ '1.07 ms ± 0.02' │ '0.15 ms ± 0.02' │ '10.46 ms ± 0.11' │
|
|
193
|
+
│ busboy │ '0.06 ms ± 0.17' │ '3.07 ms ± 0.24' │ '0.24 ms ± 0.05' │ '29.85 ms ± 0.18' │
|
|
194
|
+
│ @fastify/busboy │ '0.05 ms ± 0.13' │ '1.23 ms ± 0.09' │ '0.45 ms ± 0.22' │ '11.81 ms ± 0.11' │
|
|
195
|
+
└──────────────────┴──────────────────┴──────────────────┴──────────────────┴───────────────────┘
|
|
196
|
+
|
|
197
|
+
> @remix-run/multipart-parser@0.10.1 bench:bun /Users/michael/Projects/remix-the-web/packages/multipart-parser
|
|
198
|
+
> bun run ./bench/runner.ts
|
|
199
|
+
|
|
200
|
+
Platform: Darwin (24.5.0)
|
|
201
|
+
CPU: Apple M1 Pro
|
|
202
|
+
Date: 6/13/2025, 12:27:31 PM
|
|
203
|
+
Bun 1.2.13
|
|
204
|
+
┌──────────────────┬────────────────┬────────────────┬─────────────────┬─────────────────┐
|
|
205
|
+
│ │ 1 small file │ 1 large file │ 100 small files │ 5 large files │
|
|
206
|
+
├──────────────────┼────────────────┼────────────────┼─────────────────┼─────────────────┤
|
|
207
|
+
│ multipart-parser │ 0.01 ms ± 0.04 │ 0.86 ms ± 0.09 │ 0.04 ms ± 0.01 │ 8.32 ms ± 0.26 │
|
|
208
|
+
│ multipasta │ 0.02 ms ± 0.07 │ 0.87 ms ± 0.03 │ 0.25 ms ± 0.21 │ 8.27 ms ± 0.09 │
|
|
209
|
+
│ busboy │ 0.05 ms ± 0.17 │ 3.54 ms ± 0.10 │ 0.30 ms ± 0.03 │ 34.79 ms ± 0.38 │
|
|
210
|
+
│ @fastify/busboy │ 0.06 ms ± 0.18 │ 4.04 ms ± 0.08 │ 0.48 ms ± 0.06 │ 39.91 ms ± 0.37 │
|
|
211
|
+
└──────────────────┴────────────────┴────────────────┴─────────────────┴─────────────────┘
|
|
212
|
+
|
|
213
|
+
> @remix-run/multipart-parser@0.10.1 bench:deno /Users/michael/Projects/remix-the-web/packages/multipart-parser
|
|
214
|
+
> deno run --allow-sys ./bench/runner.ts
|
|
215
|
+
|
|
216
|
+
Platform: Darwin (24.5.0)
|
|
217
|
+
CPU: Apple M1 Pro
|
|
218
|
+
Date: 6/13/2025, 12:28:12 PM
|
|
219
|
+
Deno 2.3.6
|
|
220
|
+
┌──────────────────┬──────────────────┬────────────────────┬──────────────────┬─────────────────────┐
|
|
221
|
+
│ (idx) │ 1 small file │ 1 large file │ 100 small files │ 5 large files │
|
|
222
|
+
├──────────────────┼──────────────────┼────────────────────┼──────────────────┼─────────────────────┤
|
|
223
|
+
│ multipart-parser │ "0.01 ms ± 0.03" │ "1.03 ms ± 0.04" │ "0.05 ms ± 0.01" │ "10.05 ms ± 0.20" │
|
|
224
|
+
│ multipasta │ "0.02 ms ± 0.07" │ "1.04 ms ± 0.03" │ "0.16 ms ± 0.02" │ "10.10 ms ± 0.08" │
|
|
225
|
+
│ busboy │ "0.05 ms ± 0.19" │ "3.06 ms ± 0.15" │ "0.32 ms ± 0.05" │ "29.92 ms ± 0.24" │
|
|
226
|
+
│ @fastify/busboy │ "0.06 ms ± 0.14" │ "14.72 ms ± 11.42" │ "0.81 ms ± 0.20" │ "127.63 ms ± 35.77" │
|
|
227
|
+
└──────────────────┴──────────────────┴────────────────────┴──────────────────┴─────────────────────┘
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## Related Packages
|
|
231
|
+
|
|
232
|
+
- [`form-data-parser`](https://github.com/remix-run/remix/tree/main/packages/form-data-parser) - Uses `multipart-parser` internally to parse multipart requests and generate `FileUpload`s for storage
|
|
233
|
+
- [`headers`](https://github.com/remix-run/remix/tree/main/packages/headers) - Used internally to parse `Content-Disposition` and `Content-Type` metadata for each `MultipartPart`
|
|
234
|
+
|
|
235
|
+
## Credits
|
|
236
|
+
|
|
237
|
+
Thanks to Jacob Ebey who gave me several code reviews on this project prior to publishing.
|
|
238
|
+
|
|
239
|
+
## License
|
|
240
|
+
|
|
241
|
+
See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
# node-fetch-server
|
|
2
|
+
|
|
3
|
+
Build Node.js servers with web-standard Fetch API primitives. `node-fetch-server` converts Node's HTTP server interfaces into [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request)/[`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) flows that match modern runtimes.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Web Standards** - Standard [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) and [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) APIs
|
|
8
|
+
- **Node.js HTTP Integration** - Works directly with `node:http`, `node:https`, and `node:http2`
|
|
9
|
+
- **Streaming Support** - Response support with `ReadableStream`
|
|
10
|
+
- **Custom Hostname** - Configuration for deployment flexibility
|
|
11
|
+
- **Client Info** - Access to client connection info (IP address, port)
|
|
12
|
+
- **TypeScript** - Full TypeScript support with type definitions
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
npm i remix
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
Use `createRequestListener()` when you want to plug a fetch handler into a standard Node.js server:
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import * as http from 'node:http'
|
|
26
|
+
import { createRequestListener } from 'remix/node-fetch-server'
|
|
27
|
+
|
|
28
|
+
async function handler(request: Request) {
|
|
29
|
+
let url = new URL(request.url)
|
|
30
|
+
|
|
31
|
+
if (url.pathname === '/' && request.method === 'GET') {
|
|
32
|
+
return new Response('Welcome to the User API! Try GET /api/users')
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (url.pathname === '/api/users' && request.method === 'GET') {
|
|
36
|
+
return Response.json([
|
|
37
|
+
{ id: '1', name: 'Alice', email: 'alice@example.com' },
|
|
38
|
+
{ id: '2', name: 'Bob', email: 'bob@example.com' },
|
|
39
|
+
])
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return new Response('Not Found', { status: 404 })
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let server = http.createServer(createRequestListener(handler))
|
|
46
|
+
|
|
47
|
+
server.listen(3000, () => {
|
|
48
|
+
console.log('Server running at http://localhost:3000')
|
|
49
|
+
})
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Working with Request Data
|
|
53
|
+
|
|
54
|
+
Handle different types of request data using standard web APIs:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
async function handler(request: Request) {
|
|
58
|
+
let url = new URL(request.url)
|
|
59
|
+
|
|
60
|
+
// Handle JSON data
|
|
61
|
+
if (request.method === 'POST' && url.pathname === '/api/users') {
|
|
62
|
+
try {
|
|
63
|
+
let userData = await request.json()
|
|
64
|
+
|
|
65
|
+
// Validate required fields
|
|
66
|
+
if (!userData.name || !userData.email) {
|
|
67
|
+
return Response.json({ error: 'Name and email are required' }, { status: 400 })
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Create user (your implementation)
|
|
71
|
+
let newUser = {
|
|
72
|
+
id: Date.now().toString(),
|
|
73
|
+
...userData,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return Response.json(newUser, { status: 201 })
|
|
77
|
+
} catch (error) {
|
|
78
|
+
return Response.json({ error: 'Invalid JSON' }, { status: 400 })
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Handle URL search params
|
|
83
|
+
if (url.pathname === '/api/search') {
|
|
84
|
+
let query = url.searchParams.get('q')
|
|
85
|
+
let limit = parseInt(url.searchParams.get('limit') || '10')
|
|
86
|
+
|
|
87
|
+
return Response.json({
|
|
88
|
+
query,
|
|
89
|
+
limit,
|
|
90
|
+
results: [], // Your search results here
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return new Response('Not Found', { status: 404 })
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Streaming Responses
|
|
99
|
+
|
|
100
|
+
Take advantage of web-standard streaming with `ReadableStream`:
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
async function handler(request: Request) {
|
|
104
|
+
if (request.url.endsWith('/stream')) {
|
|
105
|
+
// Create a streaming response
|
|
106
|
+
let stream = new ReadableStream({
|
|
107
|
+
async start(controller) {
|
|
108
|
+
for (let i = 0; i < 5; i++) {
|
|
109
|
+
controller.enqueue(new TextEncoder().encode(`Chunk ${i}\n`))
|
|
110
|
+
await new Promise((resolve) => setTimeout(resolve, 1000))
|
|
111
|
+
}
|
|
112
|
+
controller.close()
|
|
113
|
+
},
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
return new Response(stream, {
|
|
117
|
+
headers: { 'Content-Type': 'text/plain' },
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return new Response('Not Found', { status: 404 })
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Custom Hostname Configuration
|
|
126
|
+
|
|
127
|
+
Configure custom hostnames for deployment on VPS or custom environments. `node-fetch-server` uses
|
|
128
|
+
the `host` option when constructing `request.url`.
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
import * as http from 'node:http'
|
|
132
|
+
import { createRequestListener } from 'remix/node-fetch-server'
|
|
133
|
+
|
|
134
|
+
let hostname = process.env.HOST || 'api.example.com'
|
|
135
|
+
|
|
136
|
+
async function handler(request: Request) {
|
|
137
|
+
console.log(request.url) // http://api.example.com/path
|
|
138
|
+
|
|
139
|
+
return Response.json({
|
|
140
|
+
message: 'Hello from custom domain!',
|
|
141
|
+
url: request.url,
|
|
142
|
+
})
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
let server = http.createServer(createRequestListener(handler, { host: hostname }))
|
|
146
|
+
|
|
147
|
+
server.listen(3000)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Accessing Client Information
|
|
151
|
+
|
|
152
|
+
Get client connection details (IP address, port) for logging or security:
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
import { type FetchHandler } from 'remix/node-fetch-server'
|
|
156
|
+
|
|
157
|
+
let handler: FetchHandler = async (request, client) => {
|
|
158
|
+
// Log client information
|
|
159
|
+
console.log(`Request from ${client.address}:${client.port}`)
|
|
160
|
+
|
|
161
|
+
// Use for rate limiting, geolocation, etc.
|
|
162
|
+
if (isRateLimited(client.address)) {
|
|
163
|
+
return new Response('Too Many Requests', { status: 429 })
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return Response.json({
|
|
167
|
+
message: 'Hello!',
|
|
168
|
+
yourIp: client.address,
|
|
169
|
+
})
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### HTTPS Support
|
|
174
|
+
|
|
175
|
+
Use with Node.js HTTPS module for secure connections:
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
import * as https from 'node:https'
|
|
179
|
+
import * as fs from 'node:fs'
|
|
180
|
+
import { createRequestListener } from 'remix/node-fetch-server'
|
|
181
|
+
|
|
182
|
+
let options = {
|
|
183
|
+
key: fs.readFileSync('private-key.pem'),
|
|
184
|
+
cert: fs.readFileSync('certificate.pem'),
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
let server = https.createServer(options, createRequestListener(handler))
|
|
188
|
+
|
|
189
|
+
server.listen(443, () => {
|
|
190
|
+
console.log('HTTPS Server running on port 443')
|
|
191
|
+
})
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Advanced Usage
|
|
195
|
+
|
|
196
|
+
### Low-level API
|
|
197
|
+
|
|
198
|
+
For more control over request/response handling, use the low-level API:
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
import * as http from 'node:http'
|
|
202
|
+
import { createRequest, sendResponse } from 'remix/node-fetch-server'
|
|
203
|
+
|
|
204
|
+
let server = http.createServer(async (req, res) => {
|
|
205
|
+
// Convert Node.js request to Fetch API Request
|
|
206
|
+
let request = createRequest(req, res, { host: process.env.HOST })
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
// Add custom headers or middleware logic
|
|
210
|
+
let startTime = Date.now()
|
|
211
|
+
|
|
212
|
+
// Process the request with your handler
|
|
213
|
+
let response = await handler(request)
|
|
214
|
+
// Make sure the response is mutable
|
|
215
|
+
response = new Response(response.body, response)
|
|
216
|
+
|
|
217
|
+
// Add response timing header
|
|
218
|
+
let duration = Date.now() - startTime
|
|
219
|
+
response.headers.set('X-Response-Time', `${duration}ms`)
|
|
220
|
+
|
|
221
|
+
// Send the response
|
|
222
|
+
await sendResponse(res, response)
|
|
223
|
+
} catch (error) {
|
|
224
|
+
console.error('Server error:', error)
|
|
225
|
+
res.writeHead(500, { 'Content-Type': 'text/plain' })
|
|
226
|
+
res.end('Internal Server Error')
|
|
227
|
+
}
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
server.listen(3000)
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
The low-level API provides:
|
|
234
|
+
|
|
235
|
+
- `createRequest(req, res, options)` - Converts Node.js IncomingMessage to web Request
|
|
236
|
+
- `sendResponse(res, response)` - Sends web Response using Node.js ServerResponse
|
|
237
|
+
|
|
238
|
+
This is useful for:
|
|
239
|
+
|
|
240
|
+
- Building custom middleware systems
|
|
241
|
+
- Integrating with existing Node.js code
|
|
242
|
+
- Implementing custom error handling
|
|
243
|
+
- Performance-critical applications
|
|
244
|
+
|
|
245
|
+
## Migration from Express
|
|
246
|
+
|
|
247
|
+
Transitioning from Express? Here's a comparison of common patterns:
|
|
248
|
+
|
|
249
|
+
### Basic Routing
|
|
250
|
+
|
|
251
|
+
```ts
|
|
252
|
+
// Express
|
|
253
|
+
let app = express()
|
|
254
|
+
|
|
255
|
+
app.get('/users/:id', async (req, res) => {
|
|
256
|
+
let user = await db.getUser(req.params.id)
|
|
257
|
+
if (!user) {
|
|
258
|
+
return res.status(404).json({ error: 'User not found' })
|
|
259
|
+
}
|
|
260
|
+
res.json(user)
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
app.listen(3000)
|
|
264
|
+
|
|
265
|
+
// node-fetch-server
|
|
266
|
+
import { createRequestListener } from 'remix/node-fetch-server'
|
|
267
|
+
|
|
268
|
+
async function handler(request: Request) {
|
|
269
|
+
let url = new URL(request.url)
|
|
270
|
+
let match = url.pathname.match(/^\/users\/(\w+)$/)
|
|
271
|
+
|
|
272
|
+
if (match && request.method === 'GET') {
|
|
273
|
+
let user = await db.getUser(match[1])
|
|
274
|
+
if (!user) {
|
|
275
|
+
return Response.json({ error: 'User not found' }, { status: 404 })
|
|
276
|
+
}
|
|
277
|
+
return Response.json(user)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return new Response('Not Found', { status: 404 })
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
http.createServer(createRequestListener(handler)).listen(3000)
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
## Demos
|
|
287
|
+
|
|
288
|
+
The [`demos` directory](https://github.com/remix-run/remix/tree/main/packages/node-fetch-server/demos) contains working demos:
|
|
289
|
+
|
|
290
|
+
- [`demos/http2`](https://github.com/remix-run/remix/tree/main/packages/node-fetch-server/demos/http2) - HTTP/2 server with TLS certificates
|
|
291
|
+
|
|
292
|
+
## Related Packages
|
|
293
|
+
|
|
294
|
+
- [`fetch-proxy`](https://github.com/remix-run/remix/tree/main/packages/fetch-proxy) - Build HTTP proxy servers using the web fetch API
|
|
295
|
+
|
|
296
|
+
## Benchmarks
|
|
297
|
+
|
|
298
|
+
Run the full benchmark suite:
|
|
299
|
+
|
|
300
|
+
```sh
|
|
301
|
+
pnpm run bench
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Update the benchmark results below:
|
|
305
|
+
|
|
306
|
+
```sh
|
|
307
|
+
pnpm run bench:update-readme
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
<!-- benchmarks:start -->
|
|
311
|
+
|
|
312
|
+
Last updated: 2026-04-29T17:19:30.407Z
|
|
313
|
+
|
|
314
|
+
Environment: Darwin 25.3.0, Apple M1 Pro, Node.js v24.15.0
|
|
315
|
+
|
|
316
|
+
Command: `wrk -t12 -c400 -d30s`
|
|
317
|
+
|
|
318
|
+
### Raw Throughput
|
|
319
|
+
|
|
320
|
+
Simple HTML response benchmarks without inspecting the incoming request.
|
|
321
|
+
|
|
322
|
+
| Server | Version | Requests/sec | Avg latency | Transfer/sec |
|
|
323
|
+
| ------------------------- | --------: | -----------: | ----------: | -----------: |
|
|
324
|
+
| `node:http` | `24.15.0` | `47,110` | `10.66ms` | `9.66MB` |
|
|
325
|
+
| `remix/node-fetch-server` | `0.13.0` | `43,317` | `11.69ms` | `8.80MB` |
|
|
326
|
+
| `express` | `5.2.1` | `39,752` | `13.69ms` | `9.59MB` |
|
|
327
|
+
|
|
328
|
+
### Small Body
|
|
329
|
+
|
|
330
|
+
POST benchmarks that read and print the request method, headers, and a small body.
|
|
331
|
+
|
|
332
|
+
| Server | Version | Requests/sec | Avg latency | Transfer/sec |
|
|
333
|
+
| ------------------------- | --------: | -----------: | ----------: | -----------: |
|
|
334
|
+
| `remix/node-fetch-server` | `0.13.0` | `25,430` | `24.25ms` | `5.17MB` |
|
|
335
|
+
| `node:http` | `24.15.0` | `25,088` | `23.89ms` | `5.14MB` |
|
|
336
|
+
| `express` | `5.2.1` | `22,845` | `27.16ms` | `5.51MB` |
|
|
337
|
+
|
|
338
|
+
### Large Body
|
|
339
|
+
|
|
340
|
+
POST benchmarks that read and print the request method, headers, and a 1 MB body.
|
|
341
|
+
|
|
342
|
+
| Server | Version | Requests/sec | Avg latency | Transfer/sec |
|
|
343
|
+
| ------------------------- | --------: | -----------: | ----------: | -----------: |
|
|
344
|
+
| `remix/node-fetch-server` | `0.13.0` | `1,086` | `217.69ms` | `225.87KB` |
|
|
345
|
+
| `node:http` | `24.15.0` | `1,079` | `198.67ms` | `226.54KB` |
|
|
346
|
+
| `express` | `5.2.1` | `1,022` | `216.07ms` | `252.51KB` |
|
|
347
|
+
|
|
348
|
+
<!-- benchmarks:end -->
|
|
349
|
+
|
|
350
|
+
## License
|
|
351
|
+
|
|
352
|
+
See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# node-tsx
|
|
2
|
+
|
|
3
|
+
Run Node.js with TypeScript and JSX syntax support in `.ts`, `.tsx`, and `.jsx` files.
|
|
4
|
+
|
|
5
|
+
`node-tsx` transforms supported source files before Node.js executes them, including
|
|
6
|
+
TypeScript syntax that requires JavaScript code generation such as enums, runtime
|
|
7
|
+
namespaces, and parameter properties. JSX compiler options are read from the nearest
|
|
8
|
+
`tsconfig.json` for each loaded file.
|
|
9
|
+
|
|
10
|
+
The loader does not type check, change Node.js module resolution, apply TypeScript
|
|
11
|
+
path aliases, or downlevel JavaScript syntax for older runtimes.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
npm i remix
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
Pass the `--import` flag to register the `remix/node-tsx` loader when executing `node`:
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
node --import remix/node-tsx ./server.ts
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### TypeScript configuration
|
|
28
|
+
|
|
29
|
+
Since import resolution still follows Node.js, configure type checking to match native Node loading:
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"compilerOptions": {
|
|
34
|
+
"module": "NodeNext",
|
|
35
|
+
"moduleResolution": "NodeNext",
|
|
36
|
+
"allowImportingTsExtensions": true,
|
|
37
|
+
"isolatedModules": true,
|
|
38
|
+
"verbatimModuleSyntax": true,
|
|
39
|
+
"rewriteRelativeImportExtensions": true
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
- [`module`](https://www.typescriptlang.org/tsconfig/#module) and [`moduleResolution`](https://www.typescriptlang.org/tsconfig/#moduleResolution) use `NodeNext` so TypeScript resolves modules the way Node does.
|
|
45
|
+
- [`allowImportingTsExtensions`](https://www.typescriptlang.org/tsconfig/#allowImportingTsExtensions) allows source files to import `.ts` and `.tsx` modules directly.
|
|
46
|
+
- [`isolatedModules`](https://www.typescriptlang.org/tsconfig/#isolatedModules) avoids patterns that require whole-program compilation, such as re-exporting a type without `export type`.
|
|
47
|
+
- [`verbatimModuleSyntax`](https://www.typescriptlang.org/tsconfig/#verbatimModuleSyntax) requires type-only imports and exports to be marked so runtime imports are unambiguous.
|
|
48
|
+
- [`rewriteRelativeImportExtensions`](https://www.typescriptlang.org/tsconfig/#rewriteRelativeImportExtensions) preserves a `tsc` emit path by rewriting relative `.ts` and `.tsx` imports to JavaScript extensions.
|
|
49
|
+
|
|
50
|
+
Do not enable `erasableSyntaxOnly` if you want TypeScript to accept the same
|
|
51
|
+
transform-only syntax that `node-tsx` can execute.
|
|
52
|
+
|
|
53
|
+
### Programmatic usage
|
|
54
|
+
|
|
55
|
+
#### Registering the loader
|
|
56
|
+
|
|
57
|
+
Import `remix/node-tsx` as a side effect to register the loader.
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
import 'remix/node-tsx'
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
#### Loading a module with scoped JSX support
|
|
64
|
+
|
|
65
|
+
Load a module with TypeScript and JSX syntax support scoped to its import graph:
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
import { loadModule } from 'remix/node-tsx/load-module'
|
|
69
|
+
|
|
70
|
+
let mod = await loadModule('./app/server.tsx', import.meta.url)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Related Work
|
|
74
|
+
|
|
75
|
+
- [Node.js Modules: Customization hooks](https://nodejs.org/api/module.html#customization-hooks)
|
|
76
|
+
|
|
77
|
+
## License
|
|
78
|
+
|
|
79
|
+
See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
|