fulmine.js 5.9.0 → 5.10.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/README.md +45 -27
- package/package.json +2 -4
- package/src/cli.js +109 -20
- package/src/types.d.ts +24 -0
package/README.md
CHANGED
|
@@ -18,12 +18,12 @@ import type { Request, Response } from "fulmine.js";
|
|
|
18
18
|
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:
|
|
19
19
|
|
|
20
20
|
```sh
|
|
21
|
-
npx fulmine verify # can this machine and this image even run it
|
|
22
|
-
npx fulmine migrate --dry-run # say what it would change, change nothing
|
|
23
|
-
npx fulmine migrate # do it
|
|
24
|
-
npx fulmine differences # just the list of what to check by hand
|
|
25
|
-
npx fulmine profile # what listen() decided about each route
|
|
26
|
-
npx fulmine explain /api/items # what happens when a request for that route arrives
|
|
21
|
+
npx fulmine.js verify # can this machine and this image even run it
|
|
22
|
+
npx fulmine.js migrate --dry-run # say what it would change, change nothing
|
|
23
|
+
npx fulmine.js migrate # do it
|
|
24
|
+
npx fulmine.js differences # just the list of what to check by hand
|
|
25
|
+
npx fulmine.js profile # what listen() decided about each route
|
|
26
|
+
npx fulmine.js explain /api/items # what happens when a request for that route arrives
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
See [Migrating](#migrating) for what it handles and what it deliberately does not.
|
|
@@ -61,6 +61,7 @@ See [Migrating](#migrating) for what it handles and what it deliberately does no
|
|
|
61
61
|
- [Router](#router)
|
|
62
62
|
- [Tested middlewares](#tested-middlewares)
|
|
63
63
|
- [Tested view engines](#tested-view-engines)
|
|
64
|
+
- [Examples](./examples/README.md)
|
|
64
65
|
- [Working on Fulmine](./CONTRIBUTING.md)
|
|
65
66
|
|
|
66
67
|
## Why this exists
|
|
@@ -115,22 +116,19 @@ It is likewise not affiliated with the OpenJS Foundation or the Express.js proje
|
|
|
115
116
|
|
|
116
117
|
## Migrating
|
|
117
118
|
|
|
118
|
-
In a lot of cases, replacing `require("express")` with `require("fulmine.js")` is the whole migration. `npx fulmine migrate` does that across a project:
|
|
119
|
+
In a lot of cases, replacing `require("express")` with `require("fulmine.js")` is the whole migration. `npx fulmine.js migrate` does that across a project:
|
|
119
120
|
|
|
120
121
|
```sh
|
|
121
|
-
npx fulmine migrate [dir] # defaults to the current directory
|
|
122
|
-
npx fulmine migrate --dry-run # say what it would rewrite and rewrite nothing
|
|
123
|
-
npx fulmine differences # print the list below and change nothing
|
|
122
|
+
npx fulmine.js migrate [dir] # defaults to the current directory
|
|
123
|
+
npx fulmine.js migrate --dry-run # say what it would rewrite and rewrite nothing
|
|
124
|
+
npx fulmine.js differences # print the list below and change nothing
|
|
124
125
|
```
|
|
125
126
|
|
|
126
|
-
The command is installed under both `fulmine` and `fulmine.js`. Use `fulmine`: `npx` cannot run a
|
|
127
|
-
command whose name ends in `.js` on Windows, where it exits without a word.
|
|
128
|
-
|
|
129
127
|
It also names the middlewares it found that have a faster one built in here, `compression`,
|
|
130
128
|
`body-parser` and `serve-static`, and leaves them to you: the replacement is reached through the
|
|
131
129
|
`express` import, and no rewrite can know that it is in scope where they are required.
|
|
132
130
|
|
|
133
|
-
`npx fulmine verify` is the question that comes before all of that: whether this machine, and the
|
|
131
|
+
`npx fulmine.js verify` is the question that comes before all of that: whether this machine, and the
|
|
134
132
|
image this will be deployed in, can run it at all. There is a µWebSockets.js binary underneath, and
|
|
135
133
|
a binary is built per platform, per architecture and per node ABI, and linked against glibc. An
|
|
136
134
|
Alpine base, a node version the pinned build has no binary for, a `FROM node:20-alpine` written
|
|
@@ -248,7 +246,7 @@ A single-stage `node:26-trixie-slim` image works too if you `apt-get install -y
|
|
|
248
246
|
|
|
249
247
|
## Differences from Express
|
|
250
248
|
|
|
251
|
-
- `app.listen()` returns the app rather than a separate server object, and the app answers as an `http.Server`: `app instanceof http.Server` is true, which is what the graceful shutdown wrappers and the connection trackers look for. There is still no node server underneath, the socket belongs to µWS, so what is answered is the surface and not the plumbing. There: `close()`, `address()`, `listening`, `getConnections()`, `ref()`, `unref()`, `setTimeout()` and the `keepAliveTimeout` family. Not there: nothing emits `connection`, `request` or `upgrade`, `getConnections()` counts the requests in flight rather than sockets, and the timeouts belong to µWS and are set through `uwsOptions.idleTimeout`. Anything that wants to serve its own protocol on the socket, socket.io being the usual case, still wants `app.uwsApp`.
|
|
249
|
+
- `app.listen()` returns the app rather than a separate server object, and the app answers as an `http.Server`: `app instanceof http.Server` is true, which is what the graceful shutdown wrappers and the connection trackers look for. There is still no node server underneath, the socket belongs to µWS, so what is answered is the surface and not the plumbing. There: `close()`, `address()`, `listening`, `getConnections()`, `ref()`, `unref()`, `setTimeout()` and the `keepAliveTimeout` family. Not there: nothing emits `connection`, `request` or `upgrade`, `getConnections()` counts the requests in flight rather than sockets, and the timeouts belong to µWS and are set through `uwsOptions.idleTimeout`. Anything that wants to serve its own protocol on the socket, socket.io being the usual case, still wants `app.uwsApp`. Runnable: [`examples/graceful-shutdown.js`](./examples/graceful-shutdown.js).
|
|
252
250
|
- `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.
|
|
253
251
|
- 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.
|
|
254
252
|
- **Informational responses go nowhere.** `res.writeEarlyHints()`, `res.writeContinue()` and `res.writeProcessing()` are all there, take what node's take and throw what node's throw once the head has gone out, but nothing reaches the wire: µWebSockets.js has no API for a `1xx`. They exist so that code written for Express keeps running rather than dying on "is not a function", which is the only thing a drop-in can honestly promise here. `res.addTrailers()` is the same story, and `res.setTimeout()` and `req.setTimeout()` register the listener without changing anything, since µWS runs its own idle timeout through `uwsOptions.idleTimeout`.
|
|
@@ -291,6 +289,8 @@ app.listen(3000, () => {
|
|
|
291
289
|
});
|
|
292
290
|
```
|
|
293
291
|
|
|
292
|
+
Runnable: [`examples/https.js`](./examples/https.js).
|
|
293
|
+
|
|
294
294
|
- 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.
|
|
295
295
|
- 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.
|
|
296
296
|
- 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.
|
|
@@ -325,7 +325,7 @@ arriving at a handler costs no matching at all:
|
|
|
325
325
|
That is the whole difference on a large route table: the scan grows with the table and the match
|
|
326
326
|
does not, which is why a thousand routes measure 10x and a handful measure 3x.
|
|
327
327
|
|
|
328
|
-
Two more things happen on the way in, and `npx fulmine profile` will tell you which of them your
|
|
328
|
+
Two more things happen on the way in, and `npx fulmine.js profile` will tell you which of them your
|
|
329
329
|
routes get:
|
|
330
330
|
|
|
331
331
|
```text
|
|
@@ -362,11 +362,11 @@ On top of that, a handler simple enough to be read at registration time is compi
|
|
|
362
362
|
|
|
363
363
|
`app.set("declarative responses", false)` turns the whole thing off if you would rather have Express's exact framing than the speed.
|
|
364
364
|
|
|
365
|
-
None of that is guesswork you have to do from the outside. `listen()` decides it all, and `npx fulmine profile` prints what it decided:
|
|
365
|
+
None of that is guesswork you have to do from the outside. `listen()` decides it all, and `npx fulmine.js profile` prints what it decided:
|
|
366
366
|
|
|
367
367
|
```sh
|
|
368
|
-
npx fulmine profile # the file
|
|
369
|
-
npx fulmine profile server.js # or name it
|
|
368
|
+
npx fulmine.js profile # the file "main" or the start script points at
|
|
369
|
+
npx fulmine.js profile server.js # or name it
|
|
370
370
|
```
|
|
371
371
|
|
|
372
372
|
```text
|
|
@@ -403,9 +403,9 @@ expectDeclarative(app, "/health"); // the step past native: no javascript at all
|
|
|
403
403
|
routeReport(app); // the whole list, to assert on however you like
|
|
404
404
|
```
|
|
405
405
|
|
|
406
|
-
A path is written as it was registered, `"/users/:id"` and not `"/users/7"`, and a trailing `*` names everything under a prefix. A pattern that matches no route throws too, so a misspelled path fails instead of passing quietly. The application does not need to be listening.
|
|
406
|
+
A path is written as it was registered, `"/users/:id"` and not `"/users/7"`, and a trailing `*` names everything under a prefix. A pattern that matches no route throws too, so a misspelled path fails instead of passing quietly. The application does not need to be listening. Runnable: [`examples/fast-routes.js`](./examples/fast-routes.js).
|
|
407
407
|
|
|
408
|
-
`npx fulmine explain /api/items/:id` answers the other question, the one about a single endpoint rather than about the table: how it is matched, what is copied out of the request, what runs and what each layer costs the route.
|
|
408
|
+
`npx fulmine.js explain /api/items/:id` answers the other question, the one about a single endpoint rather than about the table: how it is matched, what is copied out of the request, what runs and what each layer costs the route.
|
|
409
409
|
|
|
410
410
|
```text
|
|
411
411
|
GET /api/items/:id
|
|
@@ -425,9 +425,9 @@ The same verdict reaches the browser, per request, with `express.serverTiming()`
|
|
|
425
425
|
Server-Timing: route;desc="native", hdr;desc="not copied", db;dur=3.62, total;dur=4.66
|
|
426
426
|
```
|
|
427
427
|
|
|
428
|
-
`route;desc="native"` means µWS matched the path in C++ and the chain was worked out at startup; `route;desc="router"` means this one was matched here, layer by layer. `res.timing(name, ms, desc)` and `res.time(name, fn)` add marks of your own, and `fn` may return a promise. The duration ends where the header does, since Server-Timing goes out with the head. A route compiled into a response never enters JavaScript, so nothing times it: `npx fulmine profile` is where those are counted.
|
|
428
|
+
`route;desc="native"` means µWS matched the path in C++ and the chain was worked out at startup; `route;desc="router"` means this one was matched here, layer by layer. `res.timing(name, ms, desc)` and `res.time(name, fn)` add marks of your own, and `fn` may return a promise. The duration ends where the header does, since Server-Timing goes out with the head. A route compiled into a response never enters JavaScript, so nothing times it: `npx fulmine.js profile` is where those are counted. Runnable: [`examples/server-timing.js`](./examples/server-timing.js).
|
|
429
429
|
|
|
430
|
-
2. Do not use external `serve-static` module. Instead use built-in `express.static()` middleware, which is optimized for Fulmine. If your build already writes `.br` and `.gz` files next to the originals, `express.static(dir, { preCompressed: true })` serves those to the clients that accept them, so nothing is compressed at request time and a fraction of the bytes goes out: on a 4KB script with a brotli twin, 12 times fewer. It costs no more than serving the file itself, one `stat` per request, because the twin is looked for before the file and its own `stat` is the only one the request needs. A type that is already compressed, a woff2 or a webp, is not looked up at all, and which twins a path has is remembered for a second: `{ cache: false }` asks the disk every time, `{ cache: "5s" }` sets the window. Only their presence is remembered, never their size or mtime, so nothing is ever described by a stale number. `Vary: Accept-Encoding` is sent whether or not a twin is found, the content type stays the one the requested name implies, and each variant carries its own ETag.
|
|
430
|
+
2. Do not use external `serve-static` module. Instead use built-in `express.static()` middleware, which is optimized for Fulmine. If your build already writes `.br` and `.gz` files next to the originals, `express.static(dir, { preCompressed: true })` serves those to the clients that accept them, so nothing is compressed at request time and a fraction of the bytes goes out: on a 4KB script with a brotli twin, 12 times fewer. It costs no more than serving the file itself, one `stat` per request, because the twin is looked for before the file and its own `stat` is the only one the request needs. A type that is already compressed, a woff2 or a webp, is not looked up at all, and which twins a path has is remembered for a second: `{ cache: false }` asks the disk every time, `{ cache: "5s" }` sets the window. Only their presence is remembered, never their size or mtime, so nothing is ever described by a stale number. `Vary: Accept-Encoding` is sent whether or not a twin is found, the content type stays the one the requested name implies, and each variant carries its own ETag. Runnable: [`examples/static-precompressed.js`](./examples/static-precompressed.js).
|
|
431
431
|
|
|
432
432
|
3. Do not use `body-parser` module. Instead use built-in `express.text()`, `express.json()` etc.
|
|
433
433
|
|
|
@@ -438,6 +438,8 @@ Server-Timing: route;desc="native", hdr;desc="not copied", db;dur=3.62, total;du
|
|
|
438
438
|
app.use(express.compression({ threshold: 1024 }));
|
|
439
439
|
```
|
|
440
440
|
|
|
441
|
+
Runnable: [`examples/compression.js`](./examples/compression.js).
|
|
442
|
+
|
|
441
443
|
5. 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.
|
|
442
444
|
|
|
443
445
|
6. 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.
|
|
@@ -461,7 +463,7 @@ app.get("/", (req, res) => res.send("hello"));
|
|
|
461
463
|
app.listen(3000, () => console.log(`worker ${process.pid} listening`));
|
|
462
464
|
```
|
|
463
465
|
|
|
464
|
-
Anything held per process is now held per worker: an in-memory cache, a rate-limit counter, a session store or a `Map` of connected sockets is not shared, and needs Redis or something like it to be. `app.close()` in the primary stops the workers, and a `SIGTERM` or `SIGINT` that reaches only the primary, which is what a container sends, is passed on to them.
|
|
466
|
+
Anything held per process is now held per worker: an in-memory cache, a rate-limit counter, a session store or a `Map` of connected sockets is not shared, and needs Redis or something like it to be. `app.close()` in the primary stops the workers, and a `SIGTERM` or `SIGINT` that reaches only the primary, which is what a container sends, is passed on to them. Runnable: [`examples/cluster.js`](./examples/cluster.js).
|
|
465
467
|
|
|
466
468
|
## WebSockets
|
|
467
469
|
|
|
@@ -493,7 +495,7 @@ app.ws("/room/:id", {
|
|
|
493
495
|
- **Paths are the ones µWS matches**: literal, or with parameters that are a whole segment such as `/room/:id`. Anything else throws where it is written rather than failing to match later.
|
|
494
496
|
- **Broadcasting from outside a socket**: `app.publish(topic, message)` and `app.numSubscribers(topic)`.
|
|
495
497
|
|
|
496
|
-
A WebSocket route and an ordinary route can share a path: the upgrade goes to the WebSocket route, a plain GET goes through normal routing.
|
|
498
|
+
A WebSocket route and an ordinary route can share a path: the upgrade goes to the WebSocket route, a plain GET goes through normal routing. Runnable, with a page that opens the socket: [`examples/websocket.js`](./examples/websocket.js).
|
|
497
499
|
|
|
498
500
|
If you would rather use the `ws` module's API, [Ultimate WS](https://github.com/dimdenGD/ultimate-ws) is a drop-in replacement for it written against Ultimate Express, and Fulmine still exposes the mechanism it hooks into, but that combination is not covered by this project's tests. `app.uwsApp` also remains available for anything µWS offers that this does not.
|
|
499
501
|
|
|
@@ -524,7 +526,7 @@ function before it checks for a server, and an app here is callable. That refusa
|
|
|
524
526
|
answer. Even if it accepted the object, there is no node socket behind it to take an upgrade over,
|
|
525
527
|
so it would have failed later and more quietly. Plain HTTP keeps serving either way. This is covered
|
|
526
528
|
by `tests/tests/middlewares/socket-io.js`, which runs the same file against Express and against
|
|
527
|
-
Fulmine and compares the output.
|
|
529
|
+
Fulmine and compares the output. Runnable: [`examples/socket-io.js`](./examples/socket-io.js).
|
|
528
530
|
|
|
529
531
|
## HTTP/3
|
|
530
532
|
|
|
@@ -565,7 +567,9 @@ app.set("trust proxy protocol", true);
|
|
|
565
567
|
|
|
566
568
|
`trust proxy` and this can both be on. The preamble decides what the connection's address is, and
|
|
567
569
|
`trust proxy` then peels `X-Forwarded-For` off that, so a proxy that sends both is read the way it
|
|
568
|
-
meant.
|
|
570
|
+
meant. It is the binary v2 preamble that µWS reads, not the v1 text line, so a connection starting
|
|
571
|
+
with `PROXY TCP4 ...` is answered as a malformed request. Runnable, with a client that writes one:
|
|
572
|
+
[`examples/proxy-protocol.js`](./examples/proxy-protocol.js).
|
|
569
573
|
|
|
570
574
|
## Versioning
|
|
571
575
|
|
|
@@ -813,6 +817,20 @@ Any Express view engine should work. Here's list of engines we include in our te
|
|
|
813
817
|
- ✅ [express-handlebars](https://npmjs.com/package/express-handlebars)
|
|
814
818
|
- ✅ [swig](https://npmjs.com/package/swig)
|
|
815
819
|
|
|
820
|
+
## Examples
|
|
821
|
+
|
|
822
|
+
[`examples/`](./examples/README.md) has one runnable file per thing this does that Express does not:
|
|
823
|
+
the cluster option, `app.ws()`, socket.io through `attachApp`, the pre-compressed twins,
|
|
824
|
+
`express.compression()`, `express.serverTiming()`, TLS through `uwsOptions`, the PROXY protocol,
|
|
825
|
+
what `listen()` decided about each route, and the app answering as an `http.Server`. What an
|
|
826
|
+
Express application already does is documented by Express and is not repeated there.
|
|
827
|
+
|
|
828
|
+
```sh
|
|
829
|
+
cd examples
|
|
830
|
+
npm install
|
|
831
|
+
node websocket.js
|
|
832
|
+
```
|
|
833
|
+
|
|
816
834
|
## Working on Fulmine
|
|
817
835
|
|
|
818
836
|
How to run the suites, what each of them is for, and how to write a comparison test:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fulmine.js",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.10.0",
|
|
4
4
|
"description": "Drop-in Express 5 replacement on uWebSockets.js. Your existing middleware keeps working.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -29,9 +29,7 @@
|
|
|
29
29
|
"release:local": "node tools/release-local.js",
|
|
30
30
|
"cover:full": "nyc --silent npm run test && nyc --silent --no-clean npm run test:unit && nyc --silent --no-clean npm run test:express && nyc report",
|
|
31
31
|
"cover:check": "nyc check-coverage --statements 94.5 --branches 90 --functions 94 --lines 94.5",
|
|
32
|
-
"
|
|
33
|
-
"demo:deploy": "cd demo && fly deploy",
|
|
34
|
-
"demo:logs": "fly logs --app fulmine-demo"
|
|
32
|
+
"examples:install": "npm --prefix examples install"
|
|
35
33
|
},
|
|
36
34
|
"engines": {
|
|
37
35
|
"node": ">=22"
|
package/src/cli.js
CHANGED
|
@@ -304,7 +304,40 @@ function walk(node, visit) {
|
|
|
304
304
|
const DEFAULT_ENTRIES = ["server.js", "app.js", "index.js", "src/server.js", "src/app.js", "src/index.js"];
|
|
305
305
|
|
|
306
306
|
/**
|
|
307
|
-
* The file
|
|
307
|
+
* The file a start script runs, when it runs node on one.
|
|
308
|
+
*
|
|
309
|
+
* "main" is about what a package exports, and a service usually exports nothing: the entry of a
|
|
310
|
+
* deployed application is far more often the one written here, which is also the only place that
|
|
311
|
+
* knows about a src/ or a bin/ the usual names do not cover.
|
|
312
|
+
*
|
|
313
|
+
* @param {unknown} script the "start" script, as package.json wrote it
|
|
314
|
+
* @returns {string|null}
|
|
315
|
+
*/
|
|
316
|
+
function entryFromScript(script) {
|
|
317
|
+
if (typeof script !== "string") {
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
const words = script.split(/\s+/).filter(Boolean);
|
|
321
|
+
if (!/^(node|nodejs)$/.test(path.basename(words[0] ?? "", ".exe"))) {
|
|
322
|
+
// ts-node, nodemon, a shell pipeline: what that runs is not a file this can load
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
for (const word of words.slice(1)) {
|
|
326
|
+
if (word.startsWith("-")) {
|
|
327
|
+
continue; // --env-file=.env, --watch, and the rest of node's own flags
|
|
328
|
+
}
|
|
329
|
+
const candidate = path.resolve(word);
|
|
330
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
|
|
331
|
+
return candidate;
|
|
332
|
+
}
|
|
333
|
+
break; // the first thing that is not a flag is the file, and it is not there
|
|
334
|
+
}
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* The file to load, from the argument, from package.json's main or start script, or from the
|
|
340
|
+
* usual names.
|
|
308
341
|
*
|
|
309
342
|
* @param {string|undefined} given
|
|
310
343
|
* @returns {string|null}
|
|
@@ -319,6 +352,12 @@ function findEntry(given) {
|
|
|
319
352
|
if (pkg.main && fs.existsSync(path.resolve(pkg.main))) {
|
|
320
353
|
return path.resolve(pkg.main);
|
|
321
354
|
}
|
|
355
|
+
// a main that names a file nobody built, dist/server.js in a TypeScript project, is worth
|
|
356
|
+
// no more than no main at all
|
|
357
|
+
const started = entryFromScript(pkg.scripts?.start);
|
|
358
|
+
if (started) {
|
|
359
|
+
return started;
|
|
360
|
+
}
|
|
322
361
|
} catch {
|
|
323
362
|
// no package.json, or one that will not parse: the usual names are still worth trying
|
|
324
363
|
}
|
|
@@ -331,6 +370,57 @@ function findEntry(given) {
|
|
|
331
370
|
return null;
|
|
332
371
|
}
|
|
333
372
|
|
|
373
|
+
/**
|
|
374
|
+
* Every build of this library the application could load, as the prototype that owns listen().
|
|
375
|
+
*
|
|
376
|
+
* The command runs from its own copy, and the application loads whichever one resolves from its
|
|
377
|
+
* own directory. That is usually the same file and sometimes is not: a global install, an
|
|
378
|
+
* `npx fulmine.js@version`, a workspace that hoisted a second copy, or the `express` name pointing
|
|
379
|
+
* here through an override. Patching only this command's copy leaves the application's own listen()
|
|
380
|
+
* to bind the port, and the command then reports that the file built nothing.
|
|
381
|
+
*
|
|
382
|
+
* An app is a callable, so its own prototype is not the one that carries the methods: walk up to
|
|
383
|
+
* whichever link owns listen.
|
|
384
|
+
*
|
|
385
|
+
* @param {string} entry
|
|
386
|
+
* @returns {any[]} the prototypes to stub, this command's copy first
|
|
387
|
+
*/
|
|
388
|
+
function listenOwners(entry) {
|
|
389
|
+
const builds = new Set([require("./index.js")]);
|
|
390
|
+
for (const specifier of [TO, FROM]) {
|
|
391
|
+
try {
|
|
392
|
+
builds.add(require(require.resolve(specifier, { paths: [path.dirname(entry), process.cwd()] })));
|
|
393
|
+
} catch {
|
|
394
|
+
// not installed next to the application, or not resolvable from there
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const owners = [];
|
|
399
|
+
for (const build of builds) {
|
|
400
|
+
if (typeof build !== "function") {
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
let app;
|
|
404
|
+
try {
|
|
405
|
+
app = build();
|
|
406
|
+
} catch {
|
|
407
|
+
continue; // not an application factory, or one that will not build without arguments
|
|
408
|
+
}
|
|
409
|
+
// real express resolves under the same two names, and has none of this to stub
|
|
410
|
+
if (typeof app._compileOptimizedRoutes !== "function") {
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
let proto = Object.getPrototypeOf(app);
|
|
414
|
+
while (proto && !Object.prototype.hasOwnProperty.call(proto, "listen")) {
|
|
415
|
+
proto = Object.getPrototypeOf(proto);
|
|
416
|
+
}
|
|
417
|
+
if (proto && !owners.includes(proto)) {
|
|
418
|
+
owners.push(proto);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return owners;
|
|
422
|
+
}
|
|
423
|
+
|
|
334
424
|
/**
|
|
335
425
|
* The applications a file builds, compiled but not listening.
|
|
336
426
|
*
|
|
@@ -348,42 +438,38 @@ function loadApps(argv, command) {
|
|
|
348
438
|
if (!entry) {
|
|
349
439
|
console.error(
|
|
350
440
|
`Nothing to ${command}: name the file that builds the application, or run this from a
|
|
351
|
-
` + "directory whose package.json main points at it."
|
|
441
|
+
` + "directory whose package.json main or start script points at it."
|
|
352
442
|
);
|
|
353
443
|
return null;
|
|
354
444
|
}
|
|
355
445
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
// the methods: walk up to whichever link owns listen
|
|
359
|
-
const express = require("./index.js");
|
|
360
|
-
let proto = Object.getPrototypeOf(express());
|
|
361
|
-
while (proto && !Object.prototype.hasOwnProperty.call(proto, "listen")) {
|
|
362
|
-
proto = Object.getPrototypeOf(proto);
|
|
363
|
-
}
|
|
364
|
-
if (!proto) {
|
|
446
|
+
const owners = listenOwners(entry);
|
|
447
|
+
if (owners.length === 0) {
|
|
365
448
|
console.error("This build of fulmine has no listen() to stand in for, which should not happen.");
|
|
366
449
|
return null;
|
|
367
450
|
}
|
|
368
451
|
|
|
369
452
|
const listened = [];
|
|
370
|
-
const
|
|
371
|
-
proto
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
453
|
+
const real = owners.map((proto) => proto.listen);
|
|
454
|
+
for (const proto of owners) {
|
|
455
|
+
proto.listen = function stubbedListen() {
|
|
456
|
+
this._compileOptimizedRoutes();
|
|
457
|
+
listened.push(this);
|
|
458
|
+
return this;
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
const restore = () => owners.forEach((proto, i) => (proto.listen = real[i]));
|
|
376
462
|
|
|
377
463
|
try {
|
|
378
464
|
require(entry);
|
|
379
465
|
} catch (e) {
|
|
380
466
|
const error = /** @type {any} */ (e);
|
|
381
|
-
|
|
467
|
+
restore();
|
|
382
468
|
console.error(`${path.relative(process.cwd(), entry)} could not be loaded:
|
|
383
469
|
${error.stack ?? error}`);
|
|
384
470
|
return null;
|
|
385
471
|
}
|
|
386
|
-
|
|
472
|
+
restore();
|
|
387
473
|
|
|
388
474
|
let apps = listened;
|
|
389
475
|
if (apps.length === 0) {
|
|
@@ -400,7 +486,10 @@ ${error.stack ?? error}`);
|
|
|
400
486
|
if (apps.length === 0) {
|
|
401
487
|
console.error(
|
|
402
488
|
`${path.relative(process.cwd(), entry)} built no application: it neither called listen() nor
|
|
403
|
-
` +
|
|
489
|
+
` +
|
|
490
|
+
`exported one. Point this at the file that does. A listen() that runs after an await is
|
|
491
|
+
` +
|
|
492
|
+
"not seen either, since this loads the file rather than waiting on what it started."
|
|
404
493
|
);
|
|
405
494
|
return null;
|
|
406
495
|
}
|
package/src/types.d.ts
CHANGED
|
@@ -77,6 +77,18 @@ declare module "fulmine.js" {
|
|
|
77
77
|
function expectDeclarative(app: Fulmine, patterns: string | string[]): void;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
// Server-Timing, carrying how the request was routed. Express has no such middleware, so
|
|
81
|
+
// like compression() there is nothing to re-export
|
|
82
|
+
interface ServerTimingOptions {
|
|
83
|
+
/** Whether to report how the request was routed. Default true. */
|
|
84
|
+
routing?: boolean;
|
|
85
|
+
/** Whether to report the time up to the head. Default true. */
|
|
86
|
+
total?: boolean;
|
|
87
|
+
/** What the total is called. Default "total". */
|
|
88
|
+
name?: string;
|
|
89
|
+
}
|
|
90
|
+
export function serverTiming(options?: ServerTimingOptions): e.RequestHandler;
|
|
91
|
+
|
|
80
92
|
export function compression(options?: CompressionOptions): e.RequestHandler;
|
|
81
93
|
export namespace compression {
|
|
82
94
|
/** The default filter: any compressible content type. */
|
|
@@ -184,3 +196,15 @@ declare module "fulmine.js" {
|
|
|
184
196
|
|
|
185
197
|
export = express;
|
|
186
198
|
}
|
|
199
|
+
|
|
200
|
+
// What express.serverTiming() hangs on the response. Express's own Response extends this global
|
|
201
|
+
// interface, which is how a middleware adds to it. They are written per request rather than on the
|
|
202
|
+
// prototype, so a route only has them where that middleware ran, which the optional marks say.
|
|
203
|
+
declare namespace Express {
|
|
204
|
+
interface Response {
|
|
205
|
+
/** Adds a mark of your own. A mark with only a description is a legal entry. */
|
|
206
|
+
timing?(name: string, duration?: number, description?: string): this;
|
|
207
|
+
/** Times a piece of work under a name. A promise is timed to where it settles. */
|
|
208
|
+
time?<T>(name: string, work: () => T): T;
|
|
209
|
+
}
|
|
210
|
+
}
|