fulmine.js 5.0.0-rc.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/EXPRESS_LICENSE +26 -0
- package/LICENSE +202 -0
- package/NOTICE +38 -0
- package/README.md +469 -0
- package/package.json +165 -0
- package/src/application.js +561 -0
- package/src/cli.js +369 -0
- package/src/declarative.js +768 -0
- package/src/index.js +71 -0
- package/src/middlewares.js +636 -0
- package/src/node-shim.js +400 -0
- package/src/request.js +807 -0
- package/src/response.js +1360 -0
- package/src/router.js +1240 -0
- package/src/types.d.ts +62 -0
- package/src/utils.js +993 -0
- package/src/view.js +172 -0
- package/src/worker.js +38 -0
package/README.md
ADDED
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
# Fulmine
|
|
2
|
+
|
|
3
|
+
A drop-in replacement for Express 5, running on [µWebSockets.js](https://github.com/uNetworking/uWebSockets.js) instead of `node:http`. Your existing middleware keeps working.
|
|
4
|
+
|
|
5
|
+
```js
|
|
6
|
+
const express = require("fulmine.js"); // instead of require("express")
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
ESM and TypeScript work the same way, named imports included:
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import express, { Router, json } from "fulmine.js";
|
|
13
|
+
import type { Request, Response } from "fulmine.js";
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
There is a command that does that replacing for you, across a whole project, and then tells you the handful of things that behave differently:
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
npx fulmine migrate --dry-run # say what it would change, change nothing
|
|
20
|
+
npx fulmine migrate # do it
|
|
21
|
+
npx fulmine differences # just the list of what to check by hand
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
See [Migrating](#migrating) for what it handles and what it deliberately does not.
|
|
25
|
+
|
|
26
|
+
[](https://nodejs.org)
|
|
27
|
+
[](./LICENSE)
|
|
28
|
+
|
|
29
|
+
## Why this exists
|
|
30
|
+
|
|
31
|
+
There are several fast HTTP servers for Node built on [µWebSockets.js](https://github.com/uNetworking/uWebSockets.js). What is scarce is one you can actually drop into an existing Express application without rewriting it.
|
|
32
|
+
|
|
33
|
+
Compatibility here is not a claim, it is a test suite. Every test runs against real Express first and then against Fulmine, and the outputs have to match byte for byte. That is what makes `helmet`, `cors`, `passport`, `morgan`, `multer`, `express-session` and the rest of the ecosystem work rather than "mostly work".
|
|
34
|
+
|
|
35
|
+
## Performance
|
|
36
|
+
|
|
37
|
+
Fulmine is faster than Express where the framework itself is doing the work, and the same speed where it is not. Both halves of that sentence matter, so here is the honest version.
|
|
38
|
+
|
|
39
|
+
**Where it is clearly faster.** Routing and dispatch, request shapes with params and query strings, connection handling. These land between 1.7x and 3x: plain routing 1.7x to 2.3x, an API endpoint with params and a query 1.8x to 2.7x, nested routers 2.2x to 2.5x, a urlencoded body 2.7x to 3x.
|
|
40
|
+
|
|
41
|
+
**Where it is a wash.** Any request whose cost is dominated by work both servers hand to the same library. A 512 KiB JSON body is `JSON.parse`, a gzipped response is zlib, a hashed upload is OpenSSL, a 5 MiB stream is memory bandwidth. On those the ratio is capped by arithmetic somewhere around 1.0x to 1.2x, and no amount of work on either server moves it. The benchmark labels those rows rather than quietly publishing them as if the two were equivalent.
|
|
42
|
+
|
|
43
|
+
Two things worth knowing before comparing numbers with anyone:
|
|
44
|
+
|
|
45
|
+
- **Node 24 moved the baseline.** Express got roughly 3x faster on the routing benchmarks between Node 22 and Node 24, while a µWS-based server barely moved, because the gain came from `node:http`. Any comparison published before mid-2026 overstates the current gap.
|
|
46
|
+
- **Ratios are not portable across runs.** GitHub's runners vary enough that the same code measures 15k or 28k req/sec on the same row. Only compare figures produced in the same run.
|
|
47
|
+
|
|
48
|
+
There is no table here on purpose. CI runs the whole benchmark on every push and every pull request
|
|
49
|
+
and posts the result where it belongs: as a comment on the commit or the pull request, and as a
|
|
50
|
+
`benchmark-summary` artifact on the run. A table pasted in here would be a snapshot of one machine
|
|
51
|
+
on one day, and would start rotting immediately. See [`benchmark/README.md`](./benchmark/README.md)
|
|
52
|
+
to run it yourself.
|
|
53
|
+
|
|
54
|
+
## Attribution
|
|
55
|
+
|
|
56
|
+
Fulmine is a derivative work of [Ultimate Express](https://github.com/dimdenGD/ultimate-express) by [@dimdenGD](https://github.com/dimdenGD), used under the Apache License 2.0. The full commit history is preserved, so the original authorship is visible in the repository itself.
|
|
57
|
+
|
|
58
|
+
**Special thanks to [@dimdenGD](https://github.com/dimdenGD).** Ultimate Express is the hard part of this project, and it was already done before Fulmine existed. Everything here stands on that work.
|
|
59
|
+
|
|
60
|
+
Fulmine is not affiliated with, endorsed by, or maintained by the authors of Ultimate Express. See [`NOTICE`](./NOTICE) for the list of significant changes.
|
|
61
|
+
|
|
62
|
+
It is likewise not affiliated with the OpenJS Foundation or the Express.js project. Express is a trademark of the OpenJS Foundation.
|
|
63
|
+
|
|
64
|
+
## Difference from similar projects
|
|
65
|
+
|
|
66
|
+
- **`ultimate-express`** is what Fulmine is derived from, and is the closest relative by far. It targets Express 4, keeps the v4 API surface and its deprecations. Fulmine targets Express 5 only, which removes the compatibility layer for everything v5 dropped, and is typed. If you are on Express 4, use `ultimate-express`.
|
|
67
|
+
- **`hyper-express`** has a similar API but is not a drop-in replacement. It implements much of the functionality differently, which produces quirks that make switching an existing application difficult, and most Express middleware is unsupported.
|
|
68
|
+
- **`uwebsockets-express`** is closer to a drop-in replacement, but misses a lot of the API, depends on Express by calling its methods under the hood, and does not use the native µWS router.
|
|
69
|
+
- **`express` on Bun** benefits from Bun using µWS for its HTTP module, but performs no µWS-specific optimizations.
|
|
70
|
+
|
|
71
|
+
## Migrating
|
|
72
|
+
|
|
73
|
+
In a lot of cases, replacing `require("express")` with `require("fulmine.js")` is the whole migration. `npx fulmine migrate` does that across a project:
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
npx fulmine migrate [dir] # defaults to the current directory
|
|
77
|
+
npx fulmine migrate --dry-run # say what it would rewrite and rewrite nothing
|
|
78
|
+
npx fulmine differences # print the list below and change nothing
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The command is installed under both `fulmine` and `fulmine.js`. Use `fulmine`: `npx` cannot run a
|
|
82
|
+
command whose name ends in `.js` on Windows, where it exits without a word.
|
|
83
|
+
|
|
84
|
+
## Differences from Express
|
|
85
|
+
|
|
86
|
+
- `app.listen()` returns the app, not an `http.Server`. There is no node server underneath, so `server.close()`, `server.address()` and anything that attaches itself to a real `http.Server` need a look. `app.close()`, `app.address()` and `app.listening` are there and do what you would expect.
|
|
87
|
+
- `case sensitive routing` is enabled by default.
|
|
88
|
+
- `x-powered-by` is disabled by default. Express sends `X-Powered-By: Express` unless you turn it off; Fulmine does not send it unless you turn it on with `app.set("x-powered-by", true)`. The header only tells anyone asking which framework is running.
|
|
89
|
+
- request body is only read for POST, PUT, PATCH and QUERY requests by default. You can add additional methods by setting `body methods` to array with uppercased methods.
|
|
90
|
+
- For HTTPS, instead of doing this:
|
|
91
|
+
|
|
92
|
+
```js
|
|
93
|
+
const https = require("https");
|
|
94
|
+
const express = require("express");
|
|
95
|
+
|
|
96
|
+
const app = express();
|
|
97
|
+
|
|
98
|
+
https
|
|
99
|
+
.createServer(
|
|
100
|
+
{
|
|
101
|
+
key: fs.readFileSync("path/to/key.pem"),
|
|
102
|
+
cert: fs.readFileSync("path/to/cert.pem")
|
|
103
|
+
},
|
|
104
|
+
app
|
|
105
|
+
)
|
|
106
|
+
.listen(3000, () => {
|
|
107
|
+
console.log("Server is running on port 3000");
|
|
108
|
+
});
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
You have to pass `uwsOptions` to the `express()` constructor:
|
|
112
|
+
|
|
113
|
+
```js
|
|
114
|
+
const express = require("fulmine.js");
|
|
115
|
+
|
|
116
|
+
const app = express({
|
|
117
|
+
uwsOptions: {
|
|
118
|
+
// https://unetworking.github.io/uWebSockets.js/generated/interfaces/AppOptions.html
|
|
119
|
+
key_file_name: "path/to/key.pem",
|
|
120
|
+
cert_file_name: "path/to/cert.pem"
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
app.listen(3000, () => {
|
|
125
|
+
console.log("Server is running on port 3000");
|
|
126
|
+
});
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
- This also applies to non-SSL HTTP too. Use `app.listen()` rather than creating a server by hand. `http.createServer(app)` does work, because the app is a request listener like Express's and answers node's requests through a shim, which is what lets `supertest`, `vhost` and anything else that calls an app keep working. But it serves those requests through `node:http` rather than through µWS, so the speed is Express's. It is there for compatibility, not for production.
|
|
130
|
+
- Node.JS max header size is 16384 bytes, while uWebSockets by default is 4096 bytes, so if you need longer headers set the env variable `UWS_HTTP_MAX_HEADERS_SIZE` to max byte count you need.
|
|
131
|
+
- uWebSockets drops a request whose body arrives slower than 16KB/s, and the timeout is not reachable from JavaScript, while Node.JS waits as long as the client needs. Uploads over very slow connections can therefore fail here and succeed on Express. A body stalled for 5 seconds still completes; one stalled for 12 seconds gets its socket reset at around 11.8 seconds.
|
|
132
|
+
|
|
133
|
+
## Performance tips
|
|
134
|
+
|
|
135
|
+
1. Fulmine tries to optimize routing as much as possible, but it's only possible if:
|
|
136
|
+
|
|
137
|
+
- `case sensitive routing` is enabled (it is by default, unlike in normal Express).
|
|
138
|
+
- the path is a plain string, or its parameters are whole segments: `/users/:id` and `/a/:b/c/:d` qualify, `/flights/:from-:to` does not, and neither does a `*splat` or a `{}` group.
|
|
139
|
+
- inside a mounted router, nothing registered after the route in that router could match the same path. `/orders/:id`, `/orders/:id/items` and `/invoices/:id` are all optimized together, since no request reaches two of them. `/users/:id` followed by `/users/me` is not: Express answers `/users/me` with the first of the two and the native router would answer it with the second, so both go the ordinary way.
|
|
140
|
+
|
|
141
|
+
Optimized routes can be up to 10 times faster than normal routes, as they're using native uWS router and have pre-calculated path.
|
|
142
|
+
|
|
143
|
+
On top of that, a handler simple enough to be read at registration time is compiled into a uWS declarative response and answered natively, without entering JavaScript at all. That needs the route to have nothing in front of it, not a middleware and not a `Router` it was mounted under, and a single handler that only calls `res.status`, `res.set`, `res.append`, `res.send`, `res.json`, `res.sendStatus` or `res.end` with literal arguments, plus `req.params` and `req.query`. Anything else, a variable, a call, an `if`, falls back to ordinary routing. `return res.send(...)` compiles, `res.send(...)` does too, and so does an object or an array of literals however deeply nested. Mounting a `Router` costs only this: the routes inside one are still registered on the native uWS router with their full path, and are as fast as any other optimized route. Three things follow from the response being static:
|
|
144
|
+
|
|
145
|
+
- it cannot answer `304 Not Modified`. The ETag is still sent, so caches keep working, but a conditional request gets the whole body back rather than an empty 304. Express replies 304 there.
|
|
146
|
+
- it is framed as `Transfer-Encoding: chunked` and carries no `Content-Length`, because uWS writes that framing itself.
|
|
147
|
+
- it answers `Connection: keep-alive` even to a request that asked for `Connection: close`. The connection is still closed, since uWS decides that itself, and a client that asked to close is closing anyway.
|
|
148
|
+
|
|
149
|
+
`app.set("declarative responses", false)` turns the whole thing off if you would rather have Express's exact framing than the speed.
|
|
150
|
+
|
|
151
|
+
2. Do not use external `serve-static` module. Instead use built-in `express.static()` middleware, which is optimized for Fulmine.
|
|
152
|
+
|
|
153
|
+
3. Do not use `body-parser` module. Instead use built-in `express.text()`, `express.json()` etc.
|
|
154
|
+
|
|
155
|
+
4. If a route answers with a JSON shape you know in advance, [express-fast-json-stringify](https://www.npmjs.com/package/express-fast-json-stringify) compiles that shape into a serializer and `res.fastJson()` replaces `res.json()`. `JSON.stringify()` has to walk an object it knows nothing about; a compiled serializer does not.
|
|
156
|
+
|
|
157
|
+
5. Do not set `body methods` to read body of requests with GET method or other methods that don't need a body. Reading body makes endpoint about 15% slower.
|
|
158
|
+
|
|
159
|
+
6. `app.set("etag", false)` is worth about 8% on small responses, measured on both Fulmine and Express, which pay it almost identically. Know what you are trading: without an ETag a client cannot make a conditional request, so there are no `304 Not Modified` replies and every response is downloaded in full. On anything cacheable the bandwidth a 304 saves is usually worth far more than the 8%. It is left on by default for that reason. Turn it off for an API whose responses are never revalidated.
|
|
160
|
+
|
|
161
|
+
7. By default, Fulmine creates 1 (or 0 if your CPU has only 1 core) child thread to improve performance of reading files. You can change this number by setting `threads` to a different number in `express()`, or set to 0 to disable thread pool (`express({ threads: 0 })`). Threads are shared between all express() instances, with largest `threads` number being used. Using more threads will not necessarily improve performance. Sometimes not using threads at all is faster, so measure both.
|
|
162
|
+
|
|
163
|
+
## WebSockets
|
|
164
|
+
|
|
165
|
+
Since you don't create http server manually, you can't properly use http.on("upgrade") to handle WebSockets. To solve this, there's currently 2 options:
|
|
166
|
+
|
|
167
|
+
- [Ultimate WS](https://github.com/dimdenGD/ultimate-ws) implements a `ws` compatible API on the same idea: a drop-in replacement for the `ws` module. It was written against Ultimate Express and hooks into the same upgrade mechanism, which Fulmine still exposes, but that combination is not covered by this project's tests. There's a guide for how to upgrade http requests in the documentation.
|
|
168
|
+
- You can simply use `app.uwsApp` to access uWebSockets.js `App` instance and call its `ws()` method directly.
|
|
169
|
+
|
|
170
|
+
### socket.io
|
|
171
|
+
|
|
172
|
+
socket.io normally takes over the upgrade on a node `http.Server`. There isn't one here, so hand it
|
|
173
|
+
the µWS app instead, which socket.io supports natively through `attachApp()`:
|
|
174
|
+
|
|
175
|
+
```js
|
|
176
|
+
const express = require("fulmine.js");
|
|
177
|
+
const { Server } = require("socket.io");
|
|
178
|
+
|
|
179
|
+
const app = express();
|
|
180
|
+
const io = new Server();
|
|
181
|
+
|
|
182
|
+
app.listen(3000);
|
|
183
|
+
io.attachApp(app.uwsApp);
|
|
184
|
+
|
|
185
|
+
io.on("connection", (socket) => {
|
|
186
|
+
socket.on("message", (data) => socket.emit("reply", data));
|
|
187
|
+
});
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
`attachApp()` works before or after `app.listen()`. What does not work is `new Server(server)` on the
|
|
191
|
+
value `app.listen()` returns: plain HTTP keeps serving, but the WebSocket upgrade fails, because
|
|
192
|
+
that object is not a real `http.Server`. This is covered by `tests/tests/middlewares/socket-io.js`,
|
|
193
|
+
which runs the same file against Express and against Fulmine and compares the output.
|
|
194
|
+
|
|
195
|
+
## HTTP/3
|
|
196
|
+
|
|
197
|
+
HTTP/3 is supported. To use:
|
|
198
|
+
|
|
199
|
+
```js
|
|
200
|
+
const app = express({
|
|
201
|
+
http3: true,
|
|
202
|
+
uwsOptions: {
|
|
203
|
+
key_file_name: "/path/to/example.key",
|
|
204
|
+
cert_file_name: "/path/to/example.crt"
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Versioning
|
|
210
|
+
|
|
211
|
+
**The major number tracks Express, not semver.** Fulmine 5.x follows Express 5. If Express 6
|
|
212
|
+
arrives, Fulmine goes to 6, and that is the only reason the major ever moves.
|
|
213
|
+
|
|
214
|
+
Read the rest of the number normally: minor for new behaviour, patch for fixes.
|
|
215
|
+
|
|
216
|
+
What this costs you: a breaking change can land in a minor. It will be in the changelog under its
|
|
217
|
+
own heading, because commits still mark breaking changes the usual way, but the version number
|
|
218
|
+
alone will not warn you. If you pin, pin the minor.
|
|
219
|
+
|
|
220
|
+
## Compatibility
|
|
221
|
+
|
|
222
|
+
In general, basically all features and options are supported. Use the [Express 5.x documentation](https://expressjs.com/en/5x/api.html) for API reference. Anything Express 5 removed is removed here too, so the list below covers only where this differs from Express 5 itself.
|
|
223
|
+
|
|
224
|
+
✅ - Full support (all features and options are supported)
|
|
225
|
+
🚧 - Partial support (some options are not supported)
|
|
226
|
+
❌ - Not supported
|
|
227
|
+
|
|
228
|
+
### express
|
|
229
|
+
|
|
230
|
+
- ✅ express()
|
|
231
|
+
- ✅ express.Router()
|
|
232
|
+
- ✅ express.json()
|
|
233
|
+
- ✅ express.urlencoded()
|
|
234
|
+
- ✅ express.static()
|
|
235
|
+
- ✅ express.text()
|
|
236
|
+
- ✅ express.raw()
|
|
237
|
+
- 🚧 express.request (this is not a constructor but a prototype for replacing methods)
|
|
238
|
+
- 🚧 express.response (this is not a constructor but a prototype for replacing methods)
|
|
239
|
+
- 🚧 express.application (likewise: a method added here is on every app)
|
|
240
|
+
- ❌ express.Route. `app.route("/path").get(...).post(...)` works and is what almost everyone means by this; what is missing is the class itself, for constructing a route and wiring it up by hand.
|
|
241
|
+
|
|
242
|
+
### Application
|
|
243
|
+
|
|
244
|
+
- ✅ app.listen(port[, host][, callback])
|
|
245
|
+
- ✅ app.listen(unix_socket[, callback])
|
|
246
|
+
- ✅ app.METHOD() (app.get, app.post, etc.)
|
|
247
|
+
- ✅ app.route()
|
|
248
|
+
- ✅ app.all()
|
|
249
|
+
- ✅ app.use()
|
|
250
|
+
- ✅ app.mountpath
|
|
251
|
+
- ✅ app.set()
|
|
252
|
+
- ✅ app.get()
|
|
253
|
+
- ✅ app.enable()
|
|
254
|
+
- ✅ app.disable()
|
|
255
|
+
- ✅ app.enabled()
|
|
256
|
+
- ✅ app.disabled()
|
|
257
|
+
- ✅ app.path()
|
|
258
|
+
- ✅ app.param(name, callback)
|
|
259
|
+
- ✅ app.engine()
|
|
260
|
+
- ✅ app.render()
|
|
261
|
+
- ✅ app.locals
|
|
262
|
+
- ✅ app.settings
|
|
263
|
+
- ✅ app.engines
|
|
264
|
+
- ✅ app.on("mount")
|
|
265
|
+
- ✅ HEAD method
|
|
266
|
+
- ✅ OPTIONS method
|
|
267
|
+
- ✅ QUERY method
|
|
268
|
+
|
|
269
|
+
### Application settings
|
|
270
|
+
|
|
271
|
+
- ✅ case sensitive routing
|
|
272
|
+
- ✅ env
|
|
273
|
+
- ✅ etag
|
|
274
|
+
- ✅ jsonp callback name
|
|
275
|
+
- ✅ json escape
|
|
276
|
+
- ✅ json replacer
|
|
277
|
+
- ✅ json spaces
|
|
278
|
+
- ✅ query parser
|
|
279
|
+
- ✅ strict routing
|
|
280
|
+
- ✅ subdomain offset
|
|
281
|
+
- ✅ trust proxy
|
|
282
|
+
- ✅ views
|
|
283
|
+
- ✅ view cache
|
|
284
|
+
- ✅ view engine
|
|
285
|
+
- ✅ x-powered-by
|
|
286
|
+
|
|
287
|
+
Two of these keep a compiled form alongside the value, which you can also set directly:
|
|
288
|
+
|
|
289
|
+
- `etag fn`, the function that produces an ETag. Setting `etag` compiles one; setting this replaces it.
|
|
290
|
+
- `query parser fn`, likewise for `query parser`.
|
|
291
|
+
|
|
292
|
+
Fulmine adds one of its own:
|
|
293
|
+
|
|
294
|
+
- `declarative responses`, on by default. Lets a simple enough handler be compiled into a native uWS response, described under [Performance tips](#performance-tips).
|
|
295
|
+
|
|
296
|
+
### Request
|
|
297
|
+
|
|
298
|
+
- ✅ implements Readable stream
|
|
299
|
+
- ✅ req.app
|
|
300
|
+
- ✅ req.baseUrl
|
|
301
|
+
- ✅ req.body
|
|
302
|
+
- ✅ req.cookies
|
|
303
|
+
- ✅ req.fresh
|
|
304
|
+
- ✅ req.hostname
|
|
305
|
+
- ✅ req.header
|
|
306
|
+
- ✅ req.headers
|
|
307
|
+
- ✅ req.headersDistinct
|
|
308
|
+
- ✅ req.rawHeaders
|
|
309
|
+
- ✅ req.ip
|
|
310
|
+
- ✅ req.ips
|
|
311
|
+
- ✅ req.method
|
|
312
|
+
- ✅ req.url
|
|
313
|
+
- ✅ req.originalUrl
|
|
314
|
+
- ✅ req.params
|
|
315
|
+
- ✅ req.path
|
|
316
|
+
- ✅ req.protocol
|
|
317
|
+
- ✅ req.query
|
|
318
|
+
- ✅ req.res
|
|
319
|
+
- ✅ req.secure
|
|
320
|
+
- ✅ req.signedCookies
|
|
321
|
+
- ✅ req.stale
|
|
322
|
+
- ✅ req.subdomains
|
|
323
|
+
- ✅ req.xhr
|
|
324
|
+
- 🚧 req.route (route implementation is different from Express)
|
|
325
|
+
- 🚧 req.connection, req.socket (only `end()`, `encrypted`, `remoteAddress`, `remotePort` and `localPort` are supported)
|
|
326
|
+
- ✅ req.accepts()
|
|
327
|
+
- ✅ req.acceptsCharsets()
|
|
328
|
+
- ✅ req.acceptsEncodings()
|
|
329
|
+
- ✅ req.acceptsLanguages()
|
|
330
|
+
- ✅ req.get()
|
|
331
|
+
- ✅ req.is()
|
|
332
|
+
- ✅ req.range()
|
|
333
|
+
|
|
334
|
+
### Response
|
|
335
|
+
|
|
336
|
+
- ✅ implements Writable stream
|
|
337
|
+
- ✅ res.app
|
|
338
|
+
- ✅ res.headersSent
|
|
339
|
+
- ✅ res.req
|
|
340
|
+
- ✅ res.locals
|
|
341
|
+
- ✅ res.append()
|
|
342
|
+
- ✅ res.attachment()
|
|
343
|
+
- ✅ res.cookie()
|
|
344
|
+
- ✅ res.clearCookie()
|
|
345
|
+
- ✅ res.download()
|
|
346
|
+
- ✅ res.end()
|
|
347
|
+
- ✅ res.format()
|
|
348
|
+
- ✅ res.getHeader(), res.get()
|
|
349
|
+
- ✅ res.json()
|
|
350
|
+
- ✅ res.jsonp()
|
|
351
|
+
- ✅ res.links()
|
|
352
|
+
- ✅ res.location()
|
|
353
|
+
- ✅ res.redirect()
|
|
354
|
+
- ✅ res.render()
|
|
355
|
+
- ✅ res.send()
|
|
356
|
+
- ✅ res.sendFile()
|
|
357
|
+
- - ✅ options.maxAge
|
|
358
|
+
- - ✅ options.root
|
|
359
|
+
- - ✅ options.lastModified
|
|
360
|
+
- - ✅ options.headers
|
|
361
|
+
- - ✅ options.dotfiles
|
|
362
|
+
- - ✅ options.acceptRanges
|
|
363
|
+
- - ✅ options.cacheControl
|
|
364
|
+
- - ✅ options.immutable
|
|
365
|
+
- - ✅ Range header
|
|
366
|
+
- - ✅ Setting ETag header
|
|
367
|
+
- - ✅ If-Match header
|
|
368
|
+
- - ✅ If-Modified-Since header
|
|
369
|
+
- - ✅ If-Unmodified-Since header
|
|
370
|
+
- - ✅ If-Range header
|
|
371
|
+
- ✅ res.sendStatus()
|
|
372
|
+
- ✅ res.header(), res.setHeader(), res.set()
|
|
373
|
+
- ✅ res.status()
|
|
374
|
+
- ✅ res.type()
|
|
375
|
+
- ✅ res.vary()
|
|
376
|
+
- ✅ res.removeHeader()
|
|
377
|
+
- ✅ res.write()
|
|
378
|
+
- ✅ res.writeHead()
|
|
379
|
+
|
|
380
|
+
### Router
|
|
381
|
+
|
|
382
|
+
- ✅ router.all()
|
|
383
|
+
- ✅ router.METHOD() (router.get, router.post, etc.)
|
|
384
|
+
- ✅ router.route()
|
|
385
|
+
- ✅ router.use()
|
|
386
|
+
- ✅ router.param(name, callback)
|
|
387
|
+
- ✅ options.caseSensitive
|
|
388
|
+
- ✅ options.strict
|
|
389
|
+
- ✅ options.mergeParams
|
|
390
|
+
|
|
391
|
+
## Tested middlewares
|
|
392
|
+
|
|
393
|
+
Almost all middlewares that are compatible with Express are compatible with Fulmine. Here's list of middlewares that we test for compatibility:
|
|
394
|
+
|
|
395
|
+
- ✅ [express-fast-json-stringify](https://npmjs.com/package/express-fast-json-stringify)
|
|
396
|
+
- ✅ [socket.io](https://npmjs.com/package/socket.io) (via `io.attachApp(app.uwsApp)`, see WebSockets above)
|
|
397
|
+
- ✅ [body-parser](https://npmjs.com/package/body-parser) (use `express.text()` etc instead for better performance)
|
|
398
|
+
- ✅ [cookie-parser](https://npmjs.com/package/cookie-parser)
|
|
399
|
+
- ✅ [cookie-session](https://npmjs.com/package/cookie-session)
|
|
400
|
+
- ✅ [compression](https://npmjs.com/package/compression)
|
|
401
|
+
- ✅ [serve-static](https://npmjs.com/package/serve-static) (use `express.static()` instead for better performance)
|
|
402
|
+
- ✅ [serve-index](https://npmjs.com/package/serve-index)
|
|
403
|
+
- ✅ [cors](https://npmjs.com/package/cors)
|
|
404
|
+
- ✅ [errorhandler](https://npmjs.com/package/errorhandler)
|
|
405
|
+
- ✅ [method-override](https://npmjs.com/package/method-override)
|
|
406
|
+
- ✅ [multer](https://npmjs.com/package/multer)
|
|
407
|
+
- ✅ [response-time](https://npmjs.com/package/response-time)
|
|
408
|
+
- ✅ [express-fileupload](https://npmjs.com/package/express-fileupload)
|
|
409
|
+
- ✅ [express-session](https://npmjs.com/package/express-session)
|
|
410
|
+
- ✅ [express-rate-limit](https://npmjs.com/package/express-rate-limit)
|
|
411
|
+
- ✅ [express-subdomain](https://npmjs.com/package/express-subdomain)
|
|
412
|
+
- ✅ [vhost](https://npmjs.com/package/vhost)
|
|
413
|
+
- ✅ [tsoa](https://github.com/lukeautry/tsoa)
|
|
414
|
+
- ✅ [express-mongo-sanitize](https://www.npmjs.com/package/express-mongo-sanitize)
|
|
415
|
+
- ✅ [helmet](https://www.npmjs.com/package/helmet)
|
|
416
|
+
- ✅ [passport](https://www.npmjs.com/package/passport)
|
|
417
|
+
- ✅ [morgan](https://www.npmjs.com/package/morgan)
|
|
418
|
+
- ✅ [swagger-ui-express](https://www.npmjs.com/package/swagger-ui-express)
|
|
419
|
+
- ✅ [graphql-http](https://www.npmjs.com/package/graphql-http)
|
|
420
|
+
- ✅ [better-sse](https://www.npmjs.com/package/better-sse)
|
|
421
|
+
- ✅ [supertest](https://www.npmjs.com/package/supertest)
|
|
422
|
+
|
|
423
|
+
## Tested view engines
|
|
424
|
+
|
|
425
|
+
Any Express view engine should work. Here's list of engines we include in our test suite:
|
|
426
|
+
|
|
427
|
+
- ✅ [ejs](https://npmjs.com/package/ejs)
|
|
428
|
+
- ✅ [pug](https://npmjs.com/package/pug)
|
|
429
|
+
- ✅ [express-dot-engine](https://npmjs.com/package/express-dot-engine)
|
|
430
|
+
- ✅ [express-art-template](https://npmjs.com/package/express-art-template)
|
|
431
|
+
- ✅ [express-handlebars](https://npmjs.com/package/express-handlebars)
|
|
432
|
+
- ✅ [swig](https://npmjs.com/package/swig)
|
|
433
|
+
|
|
434
|
+
## Working on Fulmine
|
|
435
|
+
|
|
436
|
+
```sh
|
|
437
|
+
npm test # the comparison suite: every test runs against Express, then against
|
|
438
|
+
# Fulmine, and the two outputs have to match byte for byte
|
|
439
|
+
npm test middlewares # one category
|
|
440
|
+
npm test tests/tests/res/res-send.js # one file
|
|
441
|
+
|
|
442
|
+
npm run test:unit # the pure functions, which the comparison cannot reach
|
|
443
|
+
npm run test:types # the TypeScript declarations, through tsd
|
|
444
|
+
npm run typecheck # checkJs over src, which is where the JSDoc types are checked
|
|
445
|
+
|
|
446
|
+
npm run lint # eslint, including the rule that every function in src carries a JSDoc block
|
|
447
|
+
npm run format # prettier
|
|
448
|
+
npm run cover # the comparison suite under nyc, then an HTML report
|
|
449
|
+
|
|
450
|
+
npm run benchmark:compare -- --duration 20 # against Express, scenario by scenario
|
|
451
|
+
npm run benchmark:ab -- --against main # this working tree against another revision
|
|
452
|
+
|
|
453
|
+
npm run test:express # Express's own test suite, run against this
|
|
454
|
+
npm run test:express -- res.sendFile --verbose # one area of it, with mocha's output
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
The comparison suite is the load-bearing one. A test is a file that prints; the runner executes it
|
|
458
|
+
twice, once with `express` and once with this, and fails on any difference. That is why adding a
|
|
459
|
+
test means writing something that prints what you want compared, and why a test that prints from
|
|
460
|
+
both the server and the client at once is a bug: the two orderings are a race.
|
|
461
|
+
|
|
462
|
+
`npm run test:express` is the other kind of test: it clones Express at the version in
|
|
463
|
+
`devDependencies`, points its entry at this source and runs its suite against it. It is a bug mine
|
|
464
|
+
rather than a gate, and its exit status says nothing. Read the header of `tools/express-suite.js`
|
|
465
|
+
before reading its numbers: some of what it reports is Express testing its own internals, which the
|
|
466
|
+
clone still has, and some is internals used as public API.
|
|
467
|
+
|
|
468
|
+
`benchmark/README.md` covers measuring, including why the A/B runs pipelined by default and why a
|
|
469
|
+
null control matters.
|
package/package.json
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "fulmine.js",
|
|
3
|
+
"version": "5.0.0-rc.1",
|
|
4
|
+
"description": "Drop-in Express 5 replacement on uWebSockets.js. Your existing middleware keeps working.",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"fulmine": "src/cli.js",
|
|
8
|
+
"fulmine.js": "src/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "node tests/index.js",
|
|
12
|
+
"test:unit": "node --test \"tests/unit/*.test.js\"",
|
|
13
|
+
"test:types": "tsd --files tests/types/*.test-d.ts",
|
|
14
|
+
"test:express": "node tools/express-suite.js",
|
|
15
|
+
"benchmark:compare": "node benchmark/run.js",
|
|
16
|
+
"cover": "npm run cover:unit && npm run cover:report",
|
|
17
|
+
"cover:unit": "nyc --silent npm run test",
|
|
18
|
+
"cover:report": "nyc report --reporter=html",
|
|
19
|
+
"lint": "eslint .",
|
|
20
|
+
"lint:fix": "eslint . --fix",
|
|
21
|
+
"format": "prettier --write .",
|
|
22
|
+
"format:check": "prettier --check .",
|
|
23
|
+
"prepare": "husky",
|
|
24
|
+
"release": "release-it",
|
|
25
|
+
"typecheck": "tsc -p tsconfig.typecheck.json",
|
|
26
|
+
"benchmark:ab": "node benchmark/ab.js",
|
|
27
|
+
"benchmark:profile": "node benchmark/profile.js",
|
|
28
|
+
"release:local": "node tools/release-local.js"
|
|
29
|
+
},
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=22"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"src",
|
|
35
|
+
"LICENSE",
|
|
36
|
+
"NOTICE",
|
|
37
|
+
"EXPRESS_LICENSE"
|
|
38
|
+
],
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "git+https://github.com/nigrosimone/fulmine.js.git"
|
|
42
|
+
},
|
|
43
|
+
"keywords": [
|
|
44
|
+
"express",
|
|
45
|
+
"express5",
|
|
46
|
+
"fast",
|
|
47
|
+
"http",
|
|
48
|
+
"http server",
|
|
49
|
+
"https",
|
|
50
|
+
"https server",
|
|
51
|
+
"uwebsockets",
|
|
52
|
+
"uws",
|
|
53
|
+
"websocket",
|
|
54
|
+
"websockets",
|
|
55
|
+
"performance"
|
|
56
|
+
],
|
|
57
|
+
"types": "src/types.d.ts",
|
|
58
|
+
"author": "Nigro Simone",
|
|
59
|
+
"license": "Apache-2.0",
|
|
60
|
+
"bugs": {
|
|
61
|
+
"url": "https://github.com/nigrosimone/fulmine.js/issues"
|
|
62
|
+
},
|
|
63
|
+
"homepage": "https://github.com/nigrosimone/fulmine.js#readme",
|
|
64
|
+
"dependencies": {
|
|
65
|
+
"@types/express": "^5.0.6",
|
|
66
|
+
"accepts": "^2.0.0",
|
|
67
|
+
"acorn": "^8.16.0",
|
|
68
|
+
"bytes": "^3.1.2",
|
|
69
|
+
"content-disposition": "^1.1.0",
|
|
70
|
+
"cookie": "^1.1.1",
|
|
71
|
+
"cookie-signature": "^1.2.2",
|
|
72
|
+
"encodeurl": "^2.0.0",
|
|
73
|
+
"fast-querystring": "^1.1.2",
|
|
74
|
+
"fast-zlib": "^2.0.1",
|
|
75
|
+
"fresh": "^2.0.0",
|
|
76
|
+
"mime-types": "^3.0.2",
|
|
77
|
+
"ms": "^2.1.3",
|
|
78
|
+
"proxy-addr": "^2.0.7",
|
|
79
|
+
"qs": "^6.15.2",
|
|
80
|
+
"range-parser": "^1.2.1",
|
|
81
|
+
"statuses": "^2.0.2",
|
|
82
|
+
"tseep": "^1.3.1",
|
|
83
|
+
"type-is": "^2.1.0",
|
|
84
|
+
"uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.69.0",
|
|
85
|
+
"vary": "^1.1.2"
|
|
86
|
+
},
|
|
87
|
+
"devDependencies": {
|
|
88
|
+
"@codechecks/client": "^0.1.12",
|
|
89
|
+
"@commitlint/cli": "^21.2.1",
|
|
90
|
+
"@commitlint/config-conventional": "^21.2.0",
|
|
91
|
+
"@eslint/js": "^10.0.1",
|
|
92
|
+
"@release-it/conventional-changelog": "^12.0.0",
|
|
93
|
+
"@types/accepts": "^1.3.7",
|
|
94
|
+
"@types/bytes": "^3.1.5",
|
|
95
|
+
"@types/content-disposition": "^0.5.9",
|
|
96
|
+
"@types/cookie-signature": "^1.1.2",
|
|
97
|
+
"@types/encodeurl": "^1.0.3",
|
|
98
|
+
"@types/etag": "^1.8.4",
|
|
99
|
+
"@types/fresh": "^0.5.3",
|
|
100
|
+
"@types/mime-types": "^3.0.1",
|
|
101
|
+
"@types/ms": "^2.1.0",
|
|
102
|
+
"@types/node": "^25.9.1",
|
|
103
|
+
"@types/proxy-addr": "^2.0.3",
|
|
104
|
+
"@types/statuses": "^2.0.6",
|
|
105
|
+
"@types/type-is": "^1.6.7",
|
|
106
|
+
"@types/vary": "^1.1.3",
|
|
107
|
+
"autocannon": "^8.0.0",
|
|
108
|
+
"better-sse": "^0.16.1",
|
|
109
|
+
"body-parser": "^2.2.2",
|
|
110
|
+
"compression": "^1.8.1",
|
|
111
|
+
"cookie-parser": "^1.4.7",
|
|
112
|
+
"cookie-session": "^2.1.1",
|
|
113
|
+
"cors": "^2.8.6",
|
|
114
|
+
"ejs": "^3.1.10",
|
|
115
|
+
"errorhandler": "^1.5.2",
|
|
116
|
+
"eslint": "^10.8.0",
|
|
117
|
+
"eslint-config-prettier": "^10.1.8",
|
|
118
|
+
"eslint-plugin-jsdoc": "^63.3.2",
|
|
119
|
+
"etag": "^1.8.1",
|
|
120
|
+
"eventsource": "^4.1.0",
|
|
121
|
+
"exit-hook": "^2.2.1",
|
|
122
|
+
"express": "^5",
|
|
123
|
+
"express-art-template": "^1.0.1",
|
|
124
|
+
"express-dot-engine": "^1.0.8",
|
|
125
|
+
"express-fast-json-stringify": "^1.3.0",
|
|
126
|
+
"express-fileupload": "^1.5.2",
|
|
127
|
+
"express-handlebars": "^8.0.7",
|
|
128
|
+
"express-http-proxy": "^2.1.2",
|
|
129
|
+
"express-mongo-sanitize": "^2.2.0",
|
|
130
|
+
"express-rate-limit": "^8.5.2",
|
|
131
|
+
"express-session": "^1.19.0",
|
|
132
|
+
"express-subdomain": "^1.0.6",
|
|
133
|
+
"globals": "^17.8.0",
|
|
134
|
+
"graphql-http": "^1.22.4",
|
|
135
|
+
"helmet": "^8.2.0",
|
|
136
|
+
"http-proxy-middleware": "^3.0.5",
|
|
137
|
+
"husky": "^9.1.7",
|
|
138
|
+
"lint-staged": "^17.3.0",
|
|
139
|
+
"method-override": "^3.0.0",
|
|
140
|
+
"morgan": "^1.11.0",
|
|
141
|
+
"multer": "^2.1.1",
|
|
142
|
+
"mustache-express": "^1.3.2",
|
|
143
|
+
"nyc": "^17.1.0",
|
|
144
|
+
"pako": "^2.1.0",
|
|
145
|
+
"passport": "^0.7.0",
|
|
146
|
+
"passport-local": "^1.0.0",
|
|
147
|
+
"pkg-pr-new": "^0.0.75",
|
|
148
|
+
"prettier": "^3.9.6",
|
|
149
|
+
"pug": "^3.0.4",
|
|
150
|
+
"release-it": "^21.0.1",
|
|
151
|
+
"response-time": "^2.3.4",
|
|
152
|
+
"serve-index": "^1.9.2",
|
|
153
|
+
"serve-static": "^2.2.1",
|
|
154
|
+
"socket.io": "^4.8.3",
|
|
155
|
+
"socket.io-client": "^4.8.3",
|
|
156
|
+
"supertest": "^7.2.2",
|
|
157
|
+
"swagger-ui-express": "^5.0.1",
|
|
158
|
+
"swig": "^1.4.2",
|
|
159
|
+
"tsd": "^0.33.0",
|
|
160
|
+
"vhost": "^3.0.2"
|
|
161
|
+
},
|
|
162
|
+
"contributors": [
|
|
163
|
+
"dimden (https://github.com/dimdenGD) - author of ultimate-express, which this is derived from"
|
|
164
|
+
]
|
|
165
|
+
}
|