fulmine.js 5.2.0 → 5.4.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/NOTICE CHANGED
@@ -13,8 +13,35 @@ Fulmine is not affiliated with, endorsed by, or maintained by the authors of
13
13
  Ultimate Express.
14
14
 
15
15
  This product includes code derived from fast-querystring
16
- (https://github.com/anonrig/fast-querystring), Copyright (c) Yagiz Nizipli,
17
- licensed under the MIT License, vendored in src/parse-query.js.
16
+ (https://github.com/anonrig/fast-querystring), vendored in src/parse-query.js
17
+ and licensed under the MIT License, which requires the following to travel with
18
+ every copy:
19
+
20
+ Copyright (c) 2022 Yagiz Nizipli
21
+
22
+ Permission is hereby granted, free of charge, to any
23
+ person obtaining a copy of this software and associated
24
+ documentation files (the "Software"), to deal in the
25
+ Software without restriction, including without
26
+ limitation the rights to use, copy, modify, merge,
27
+ publish, distribute, sublicense, and/or sell copies of
28
+ the Software, and to permit persons to whom the Software
29
+ is furnished to do so, subject to the following
30
+ conditions:
31
+
32
+ The above copyright notice and this permission notice
33
+ shall be included in all copies or substantial portions
34
+ of the Software.
35
+
36
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
37
+ ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
38
+ TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
39
+ PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
40
+ SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
41
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
42
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
43
+ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
44
+ DEALINGS IN THE SOFTWARE.
18
45
 
19
46
  As required by section 4(b) of the Apache License, the following are the
20
47
  significant changes made to the original work:
package/README.md CHANGED
@@ -21,10 +21,17 @@ There is a command that does that replacing for you, across a whole project, and
21
21
  npx fulmine migrate --dry-run # say what it would change, change nothing
22
22
  npx fulmine migrate # do it
23
23
  npx fulmine differences # just the list of what to check by hand
24
+ npx fulmine profile # what listen() decided about each route
24
25
  ```
25
26
 
26
27
  See [Migrating](#migrating) for what it handles and what it deliberately does not.
27
28
 
29
+ There is a **[live demo](https://fulmine-demo.fly.dev)**, which is an ordinary Express application:
30
+ real routes, `helmet`, `cors`, `compression`, `express-session` and `morgan` unmodified, and a
31
+ WebSocket chat served by `app.ws()`. It links to [its own source](https://fulmine-demo.fly.dev/source),
32
+ which is [in this repository](./demo). It shows no throughput figure on purpose: it runs on a small
33
+ shared machine, so the number would describe the machine rather than the framework.
34
+
28
35
  [![npm version](https://img.shields.io/npm/v/fulmine.js)](https://www.npmjs.com/package/fulmine.js)
29
36
  [![Node.js >= 22.0.0](https://img.shields.io/badge/Node.js-%3E=22.0.0-green)](https://nodejs.org)
30
37
  [![Coverage Status](https://coveralls.io/repos/github/nigrosimone/fulmine.js/badge.svg?branch=main)](https://coveralls.io/github/nigrosimone/fulmine.js?branch=main)
@@ -39,12 +46,14 @@ See [Migrating](#migrating) for what it handles and what it deliberately does no
39
46
  - [Attribution](#attribution)
40
47
  - [Difference from similar projects](#difference-from-similar-projects)
41
48
  - [Migrating](#migrating)
49
+ - [When Express is somebody else's dependency](#when-express-is-somebody-elses-dependency)
42
50
  - [Docker](#docker)
43
51
  - [Differences from Express](#differences-from-express)
44
52
  - [Performance tips](#performance-tips)
45
53
  - [WebSockets](#websockets)
46
54
  - [socket.io](#socketio)
47
55
  - [HTTP/3](#http3)
56
+ - [Behind a proxy](#behind-a-proxy)
48
57
  - [Versioning](#versioning)
49
58
  - [Compatibility](#compatibility)
50
59
  - [express](#express)
@@ -55,8 +64,8 @@ See [Migrating](#migrating) for what it handles and what it deliberately does no
55
64
  - [Router](#router)
56
65
  - [Tested middlewares](#tested-middlewares)
57
66
  - [Tested view engines](#tested-view-engines)
58
- - [Working on Fulmine](#working-on-fulmine)
59
- - [Writing a comparison test](#writing-a-comparison-test)
67
+ - [The demo](https://fulmine-demo.fly.dev)
68
+ - [Working on Fulmine](./CONTRIBUTING.md)
60
69
 
61
70
  ## Why this exists
62
71
 
@@ -121,22 +130,64 @@ npx fulmine differences # print the list below and change nothing
121
130
  The command is installed under both `fulmine` and `fulmine.js`. Use `fulmine`: `npx` cannot run a
122
131
  command whose name ends in `.js` on Windows, where it exits without a word.
123
132
 
133
+ ### When Express is somebody else's dependency
134
+
135
+ A framework built on Express does not `require("express")` in your code, it requires it in its own,
136
+ so there is nothing for `migrate` to rewrite. Every package manager can answer `express` with this
137
+ package instead, for your project and everything under it:
138
+
139
+ ```jsonc
140
+ // npm and its lockfile, in package.json
141
+ {
142
+ "overrides": {
143
+ "express": "npm:fulmine.js@^5"
144
+ }
145
+ }
146
+
147
+ // pnpm, in package.json
148
+ {
149
+ "pnpm": {
150
+ "overrides": {
151
+ "express": "npm:fulmine.js@^5"
152
+ }
153
+ }
154
+ }
155
+
156
+ // yarn 1 and berry, in package.json
157
+ {
158
+ "resolutions": {
159
+ "express": "npm:fulmine.js@^5"
160
+ }
161
+ }
162
+ ```
163
+
164
+ Then reinstall, so the lockfile is rewritten: `rm -rf node_modules` and `npm install`, or the
165
+ equivalent for your manager. `npm ls express` should answer `express@npm:fulmine.js`.
166
+
167
+ Two things to know before you do it. The substitution reaches **every** dependency that asks for
168
+ Express, including ones you have never looked at, so run your own tests afterwards and read
169
+ [the differences](#differences-from-express): what a framework does with Express is usually more
170
+ than what an application does. And a package that reaches into `express/lib/...` rather than its
171
+ public surface will not find what it expects, since the files there are ours.
172
+
173
+ Bun is not an option: µWebSockets.js is a native Node addon, and Bun does not load it.
174
+
124
175
  ## Docker
125
176
 
126
177
  Two things about µWebSockets.js make a Dockerfile that works for Express fail here, and both have easy answers:
127
178
 
128
- - **No Alpine, and no Debian bookworm either.** µWebSockets.js ships prebuilt binaries linked against glibc 2.38 or newer. Alpine images use musl, so the binary does not load at all; `node:22` and `node:22-slim` are Debian bookworm, whose glibc 2.36 fails at startup with `GLIBC_2.38' not found`. Use the trixie variants: `node:22-trixie-slim` and up.
129
- - **`git` must be there when `npm install` runs.** µWebSockets.js is not on npm; it is installed straight from GitHub (`github:uNetworking/uWebSockets.js`), and npm uses git to fetch it. Full images like `node:22-trixie` have git; `-slim` ones do not.
179
+ - **No Alpine, and no Debian bookworm either.** µWebSockets.js ships prebuilt binaries linked against glibc 2.38 or newer. Alpine images use musl, so the binary does not load at all; `node:26` and `node:26-slim` are Debian bookworm, whose glibc 2.36 fails at startup with `GLIBC_2.38' not found`. Use the trixie variants: `node:26-trixie-slim` and up.
180
+ - **`git` must be there when `npm install` runs.** µWebSockets.js is not on npm; it is installed straight from GitHub (`github:uNetworking/uWebSockets.js`), and npm uses git to fetch it. Full images like `node:26-trixie` have git; `-slim` ones do not.
130
181
 
131
182
  The clean way to satisfy both is a multi-stage build: install with the full image, run with the slim one.
132
183
 
133
184
  ```dockerfile
134
- FROM node:22-trixie AS build
185
+ FROM node:26-trixie AS build
135
186
  WORKDIR /app
136
187
  COPY package*.json ./
137
188
  RUN npm ci --omit=dev
138
189
 
139
- FROM node:22-trixie-slim
190
+ FROM node:26-trixie-slim
140
191
  WORKDIR /app
141
192
  COPY --from=build /app/node_modules ./node_modules
142
193
  COPY . .
@@ -144,7 +195,7 @@ EXPOSE 3000
144
195
  CMD ["node", "server.js"]
145
196
  ```
146
197
 
147
- A single-stage `node:22-trixie-slim` image works too if you `apt-get install -y git ca-certificates` before `npm ci`. Prebuilt binaries exist for x64 and arm64 on Linux, macOS and Windows, so nothing is compiled at install time either way.
198
+ A single-stage `node:26-trixie-slim` image works too if you `apt-get install -y git ca-certificates` before `npm ci`. Prebuilt binaries exist for x64 and arm64 on Linux, macOS and Windows, so nothing is compiled at install time either way.
148
199
 
149
200
  ## Differences from Express
150
201
 
@@ -196,6 +247,56 @@ app.listen(3000, () => {
196
247
 
197
248
  ## Performance tips
198
249
 
250
+ Where the speed comes from, before the rules that govern it. Express finds a route by walking its
251
+ stack and testing each layer against the path. Fulmine hands every route it can to µWS's own router,
252
+ which matches in C++, and works out at `listen()` which layers stand in front of each one, so
253
+ arriving at a handler costs no matching at all:
254
+
255
+ ```text
256
+ Express Fulmine
257
+ GET /users/42 GET /users/42
258
+ | |
259
+ v v
260
+ +--------------+ +------------------+
261
+ | layer 1 | path? no | µWS router | one match, in C++,
262
+ | layer 2 | path? no | /users/:id | against every path
263
+ | ... | +--------+---------+ registered
264
+ | layer 214 | path? yes -+ |
265
+ +--------------+ | v
266
+ a test per layer, | +------------------+
267
+ every request | | the chain, known | the layers in front,
268
+ | | since listen() | in order, no matching
269
+ v +--------+---------+
270
+ handler |
271
+ v
272
+ handler
273
+ ```
274
+
275
+ That is the whole difference on a large route table: the scan grows with the table and the match
276
+ does not, which is why a thousand routes measure 10x and a handful measure 3x.
277
+
278
+ Two more things happen on the way in, and `npx fulmine profile` will tell you which of them your
279
+ routes get:
280
+
281
+ ```text
282
+ a request arriving at a compiled route
283
+
284
+ µWS match ──► the chain ──────────────────────────► handler ──► response
285
+ | |
286
+ | a body parser is stepped over | the Readable is not
287
+ | when the request declared no | built unless something
288
+ | body and the verb reads none | asks the body for one
289
+ | |
290
+ | the headers are not copied out | the two internal
291
+ | of µWS when the analysis proved | listeners are written
292
+ | nothing in the chain reads one | into the event map
293
+ v v
294
+ work that does not happen work that is not prepared
295
+
296
+ and when the handler is simple enough to be read at registration time, none of the
297
+ above happens either: µWS answers from a response written once, at startup
298
+ ```
299
+
199
300
  1. Fulmine tries to optimize routing as much as possible, but it's only possible if:
200
301
 
201
302
  - 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. Routing is case-insensitive by default, as in Express; a request in the registered case is still served natively, any other case takes the ordinary path, and a route whose overlap with an earlier one leans on a cased literal goes the ordinary way for every request.
@@ -211,6 +312,37 @@ On top of that, a handler simple enough to be read at registration time is compi
211
312
 
212
313
  `app.set("declarative responses", false)` turns the whole thing off if you would rather have Express's exact framing than the speed.
213
314
 
315
+ None of that is guesswork you have to do from the outside. `listen()` decides it all, and `npx fulmine profile` prints what it decided:
316
+
317
+ ```sh
318
+ npx fulmine profile # the file package.json's "main" points at
319
+ npx fulmine profile server.js # or name it
320
+ ```
321
+
322
+ ```text
323
+ 7 route(s), 4 answered by µWS itself
324
+
325
+ GET /api/health µWS /api/health (2 in front of it in its chain)
326
+ GET /hello µWS /hello (compiled to a response, reads no query)
327
+ GET /:anything router: something before it in the same router overlaps its paths
328
+ GET /after-the-param router: the parameter route /:anything is written before it
329
+ SEARCH /odd router: µWS does not serve SEARCH
330
+
331
+ What this adds up to
332
+
333
+ 4 of 7 route(s) matched by µWS in C++
334
+ 1 answered from a response written at startup, running no javascript
335
+ layers in front of a compiled handler: 1 at least, 2 at most, 1.8 on average
336
+
337
+ Worth changing, if these are routes that carry traffic
338
+
339
+ GET /after-the-param
340
+ write it above /:anything. Express answers whichever matches first, so the order is
341
+ already what decides, and with the literal first µWS can match it in C++ as well.
342
+ ```
343
+
344
+ It loads the application with `listen()` replaced by the half that compiles the routes, so nothing binds a port and the listen callback does not run: profiling a running service does not start a second copy of it. There is no score, on purpose. A percentage of routes is not a percentage of traffic, and an application with a thousand cold routes and one hot one that fell back would score well and serve badly.
345
+
214
346
  2. Do not use external `serve-static` module. Instead use built-in `express.static()` middleware, which is optimized for Fulmine.
215
347
 
216
348
  3. Do not use `body-parser` module. Instead use built-in `express.text()`, `express.json()` etc.
@@ -297,6 +429,32 @@ const app = express({
297
429
  });
298
430
  ```
299
431
 
432
+ ## Behind a proxy
433
+
434
+ `trust proxy` works as it does in Express: set it and `req.ip`, `req.ips`, `req.protocol` and
435
+ `req.hostname` are read from `X-Forwarded-*` when the connection comes from a peer you trust.
436
+
437
+ Fulmine adds the other way of being told, the one that does not use headers at all. HAProxy, AWS
438
+ NLB, nginx with `proxy_protocol` and Envoy can prepend a **PROXY protocol** preamble to the
439
+ connection, and µWebSockets.js parses it. Off by default, and one line turns it on:
440
+
441
+ ```js
442
+ app.set("trust proxy protocol", true);
443
+ // req.ip, req.socket.remoteAddress and everything reading them are now the address the proxy
444
+ // declared, and fall back to the socket's own on a connection that sent no preamble
445
+ ```
446
+
447
+ > [!WARNING]
448
+ > **Only turn this on when nothing but the proxy can reach the server.** µWS reads the preamble
449
+ > from whoever sends it. There is no way to say which peers may use it, so on a port open to the
450
+ > internet the first sixteen bytes of any connection are enough for a client to become `10.0.0.1`
451
+ > for your rate limiter, your allow list and your audit log. Bind to the private interface, or
452
+ > keep this off.
453
+
454
+ `trust proxy` and this can both be on. The preamble decides what the connection's address is, and
455
+ `trust proxy` then peels `X-Forwarded-For` off that, so a proxy that sends both is read the way it
456
+ meant.
457
+
300
458
  ## Versioning
301
459
 
302
460
  **The major number tracks Express, not semver.** Fulmine 5.x follows Express 5. If Express 6
@@ -380,9 +538,11 @@ Two of these keep a compiled form alongside the value, which you can also set di
380
538
  - `etag fn`, the function that produces an ETag. Setting `etag` compiles one; setting this replaces it.
381
539
  - `query parser fn`, likewise for `query parser`.
382
540
 
383
- Fulmine adds one of its own:
541
+ Fulmine adds three of its own:
384
542
 
385
543
  - `declarative responses`, on by default. Lets a simple enough handler be compiled into a native uWS response, described under [Performance tips](#performance-tips).
544
+ - `file cache`, on by default. Small files served by `res.sendFile` come from a bounded in-process cache, checked against the file's `stat` on every request, so an edited file is never served stale.
545
+ - `trust proxy protocol`, off by default. Takes `req.ip` from a PROXY protocol preamble, described under [Behind a proxy](#behind-a-proxy). Read the warning there before turning it on.
386
546
 
387
547
  ### Request
388
548
 
@@ -501,7 +661,8 @@ Almost all middlewares that are compatible with Express are compatible with Fulm
501
661
  - ✅ [express-rate-limit](https://npmjs.com/package/express-rate-limit)
502
662
  - ✅ [express-subdomain](https://npmjs.com/package/express-subdomain)
503
663
  - ✅ [vhost](https://npmjs.com/package/vhost)
504
- - ✅ [tsoa](https://github.com/lukeautry/tsoa)
664
+ - ✅ [http-proxy-middleware](https://www.npmjs.com/package/http-proxy-middleware)
665
+ - ✅ [express-http-proxy](https://www.npmjs.com/package/express-http-proxy)
505
666
  - ✅ [express-mongo-sanitize](https://www.npmjs.com/package/express-mongo-sanitize)
506
667
  - ✅ [helmet](https://www.npmjs.com/package/helmet)
507
668
  - ✅ [passport](https://www.npmjs.com/package/passport)
@@ -511,6 +672,10 @@ Almost all middlewares that are compatible with Express are compatible with Fulm
511
672
  - ✅ [better-sse](https://www.npmjs.com/package/better-sse)
512
673
  - ✅ [supertest](https://www.npmjs.com/package/supertest)
513
674
 
675
+ [tsoa](https://github.com/lukeautry/tsoa) works too, but it is not in the suite above: it resolves
676
+ `express` itself, so testing it here needs a dependency override rather than the one-line swap
677
+ everything else takes.
678
+
514
679
  ## Tested view engines
515
680
 
516
681
  Any Express view engine should work. Here's list of engines we include in our test suite:
@@ -524,57 +689,5 @@ Any Express view engine should work. Here's list of engines we include in our te
524
689
 
525
690
  ## Working on Fulmine
526
691
 
527
- ```sh
528
- npm test # the comparison suite: every test runs against Express, then against
529
- # Fulmine, and the two outputs have to match byte for byte
530
- npm test middlewares # one category
531
- npm test tests/tests/res/res-send.js # one file
532
-
533
- npm run test:unit # the pure functions, which the comparison cannot reach
534
- npm run test:types # the TypeScript declarations, through tsd
535
- npm run typecheck # checkJs over src, which is where the JSDoc types are checked
536
-
537
- npm run lint # eslint, including the rule that every function in src carries a JSDoc block
538
- npm run format # prettier
539
- npm run cover # the comparison suite under nyc, then an HTML report
540
-
541
- npm run benchmark:compare -- --duration 20 # against Express, scenario by scenario
542
- npm run benchmark:ab -- --against main # this working tree against another revision
543
-
544
- npm run test:express # Express's own test suite, run against this
545
- npm run test:express -- res.sendFile --verbose # one area of it, with mocha's output
546
- ```
547
-
548
- The comparison suite is the load-bearing one. A test is a file that prints; the runner executes it
549
- twice, once with `express` and once with this, and fails on any difference. That is why adding a
550
- test means writing something that prints what you want compared, and why a test that prints from
551
- both the server and the client at once is a bug: the two orderings are a race.
552
-
553
- ### Writing a comparison test
554
-
555
- A test file is an ordinary script. The first line is its description, the second may carry a marker,
556
- and the rest sets up an app, makes requests and prints. `tests/helpers.js` has what to print with:
557
-
558
- | | |
559
- | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
560
- | `fetchTest(url, init)` | `fetch`, plus a line with the status and the headers worth comparing. Returns the response untouched, so the test goes on to read the body as it would have. Lines come out in call order, never in arrival order. |
561
- | `sequential([() => …])` | Runs requests one at a time. `Promise.all` starts them together and the two servers then answer in whatever order they scheduled, which is a difference the runner would report as a failure. |
562
- | `// INSPECT` | On the second line. The runner then mounts `inspectRequest` in front of every app the file makes, and each request prints its `method`, `url`, `originalUrl`, `baseUrl`, `path`, `protocol`, `secure`, `hostname`, `host`, `xhr`, `subdomains` and `query`. |
563
- | `// OFF: reason` | Skips the file. |
564
-
565
- `// INSPECT` is not free everywhere, which is why it is asked for rather than always on. It is a
566
- middleware, so a route behind it stops being compiled into a declarative response and is served by
567
- the ordinary path instead: a file whose routes do compile would quietly stop covering the compiled
568
- one. And Express builds its router at the first `use()`, freezing `strict routing` and
569
- `case sensitive routing` as they are at that moment, so a file that sets either one afterwards must
570
- not ask for it. Everywhere else it is worth having: it is what caught a pathless mount dropping the
571
- middleware in front of it.
572
-
573
- `npm run test:express` is the other kind of test: it clones Express at the version in
574
- `devDependencies`, points its entry at this source and runs its suite against it. It is a bug mine
575
- rather than a gate, and its exit status says nothing. Read the header of `tools/express-suite.js`
576
- before reading its numbers: some of what it reports is Express testing its own internals, which the
577
- clone still has, and some is internals used as public API.
578
-
579
- `benchmark/README.md` covers measuring, including why the A/B runs pipelined by default and why a
580
- null control matters.
692
+ How to run the suites, what each of them is for, and how to write a comparison test:
693
+ [`CONTRIBUTING.md`](./CONTRIBUTING.md).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fulmine.js",
3
- "version": "5.2.0",
3
+ "version": "5.4.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": {
@@ -12,6 +12,7 @@
12
12
  "test:unit": "node --test \"tests/unit/*.test.js\"",
13
13
  "test:types": "tsd --files tests/types/*.test-d.ts",
14
14
  "test:express": "node tools/express-suite.js",
15
+ "fuzz": "node tools/fuzz.js",
15
16
  "benchmark:compare": "node benchmark/run.js",
16
17
  "cover": "npm run cover:full && npm run cover:report",
17
18
  "cover:unit": "nyc --silent npm run test",
@@ -27,7 +28,10 @@
27
28
  "benchmark:profile": "node benchmark/profile.js",
28
29
  "release:local": "node tools/release-local.js",
29
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",
30
- "cover:check": "nyc check-coverage --statements 93 --branches 88 --functions 92 --lines 93"
31
+ "cover:check": "nyc check-coverage --statements 93 --branches 88 --functions 92 --lines 93",
32
+ "demo:start": "npm --prefix demo install && npm --prefix demo start",
33
+ "demo:deploy": "cd demo && fly deploy",
34
+ "demo:logs": "fly logs --app fulmine-demo"
31
35
  },
32
36
  "engines": {
33
37
  "node": ">=22"
@@ -124,6 +128,7 @@
124
128
  "exit-hook": "^2.2.1",
125
129
  "express": "^5",
126
130
  "express-art-template": "^1.0.1",
131
+ "express-basic-auth": "^1.2.1",
127
132
  "express-dot-engine": "^1.0.8",
128
133
  "express-fast-json-stringify": "^1.3.0",
129
134
  "express-fileupload": "^1.5.2",
@@ -133,6 +138,7 @@
133
138
  "express-rate-limit": "^8.5.2",
134
139
  "express-session": "^1.19.0",
135
140
  "express-subdomain": "^1.0.6",
141
+ "express-validator": "^7.3.2",
136
142
  "fast-querystring": "^1.1.2",
137
143
  "globals": "^17.8.0",
138
144
  "graphql-http": "^1.22.4",
@@ -145,6 +151,8 @@
145
151
  "multer": "^2.1.1",
146
152
  "mustache-express": "^1.3.2",
147
153
  "nyc": "^17.1.0",
154
+ "on-finished": "^2.4.1",
155
+ "on-headers": "^1.1.0",
148
156
  "pako": "^2.1.0",
149
157
  "passport": "^0.7.0",
150
158
  "passport-local": "^1.0.0",
@@ -153,6 +161,7 @@
153
161
  "pug": "^3.0.4",
154
162
  "release-it": "^21.0.1",
155
163
  "response-time": "^2.3.4",
164
+ "serve-favicon": "^2.5.1",
156
165
  "serve-index": "^1.9.2",
157
166
  "serve-static": "^2.2.1",
158
167
  "socket.io": "^4.8.3",
@@ -2,6 +2,8 @@
2
2
  Copyright 2024 dimden.dev
3
3
  Copyright 2026 Nigro Simone
4
4
 
5
+ This file is derived from Ultimate Express and has been modified.
6
+
5
7
  Licensed under the Apache License, Version 2.0 (the "License");
6
8
  you may not use this file except in compliance with the License.
7
9
  You may obtain a copy of the License at
@@ -83,6 +85,20 @@ const FILE_CACHE_MAX_ENTRY = 768 * 1024;
83
85
  const FILE_CACHE_BUDGET = 64 * 1024 * 1024;
84
86
 
85
87
  class Application extends Router {
88
+ /**
89
+ * An application reads an unset routing flag from the app it is mounted on, which a plain
90
+ * Router does not: express chains a mounted app's settings onto its parent's.
91
+ *
92
+ * @type {boolean}
93
+ */
94
+ _inheritsSettings = true;
95
+
96
+ /**
97
+ * An application, which a plain Router is not. See Router#_isApplication.
98
+ * @type {boolean}
99
+ */
100
+ _isApplication = true;
101
+
86
102
  /**
87
103
  * @param {object} [settings] the options express() takes. uwsOptions goes to uWS and decides
88
104
  * between an HTTP, an HTTPS and an HTTP/3 server; threads sizes the file-reading pool, and 0
@@ -171,6 +187,12 @@ class Application extends Router {
171
187
  if (parent.response) {
172
188
  Object.setPrototypeOf(this.response, parent.response);
173
189
  }
190
+ // and the engines with them, which is the same chaining express does: a sub-app renders
191
+ // with whatever the parent registered unless it registered its own. Without this a
192
+ // render inside a mounted app looked for a module named after the extension.
193
+ if (parent.engines) {
194
+ Object.setPrototypeOf(this.engines, parent.engines);
195
+ }
174
196
  // a "trust proxy" this app never set is inherited from the parent, as express does:
175
197
  // the defaults are deleted so get() falls through to the parent's value
176
198
  if (
@@ -357,10 +379,6 @@ class Application extends Router {
357
379
  // express's wording, which applications match on
358
380
  throw new TypeError("unknown value for query parser function: " + value);
359
381
  }
360
- } else if (key === "views") {
361
- // a list of directories is searched in order by View.lookup, each resolved here once
362
- this.settings[key] = Array.isArray(value) ? value.map((dir) => path.resolve(dir)) : path.resolve(value);
363
- return this;
364
382
  } else if (key === "etag") {
365
383
  // an etag arriving after listen would make send consult freshness headers the
366
384
  // header-skip routes never copied, so those skips are taken back
@@ -417,13 +435,13 @@ class Application extends Router {
417
435
  }
418
436
 
419
437
  /**
420
- * Whether a setting is truthy. Reads this app's own settings, without falling back to a
421
- * parent app the way get() does.
438
+ * Whether a setting is truthy. Reads through to the app this one is mounted on, as get() does
439
+ * and as express does: mounting chains a sub-app's settings onto its parent's.
422
440
  * @param {string} key setting name
423
441
  * @returns {boolean}
424
442
  */
425
443
  enabled(key) {
426
- return !!this.settings[key];
444
+ return !!this.get(key);
427
445
  }
428
446
 
429
447
  /**
@@ -432,7 +450,7 @@ class Application extends Router {
432
450
  * @returns {boolean}
433
451
  */
434
452
  disabled(key) {
435
- return !this.settings[key];
453
+ return !this.get(key);
436
454
  }
437
455
 
438
456
  /**
@@ -471,32 +489,42 @@ class Application extends Router {
471
489
  * between an error, the automatic OPTIONS reply and a 404.
472
490
  */
473
491
  _createRequestHandler() {
474
- this.uwsApp.any("/*", async (res, req) => {
475
- const request = this.handleRequest(res, req);
476
- const response = request.res;
477
- // armed up front here: this handler awaits, so the response outlives the callback
478
- // on every path through it
479
- this._armAbort(res, response);
480
-
481
- try {
482
- const routed = this._routeRequest(request, response);
483
- // dispatch has run its synchronous stretch inside _routeRequest by now, still
484
- // under the cork uWS holds for this callback; the await below leaves it
485
- response._corkNeeded = true;
486
- const matchedRoute = await routed;
487
- if (!matchedRoute && !response.headersSent && !response.aborted) {
488
- this._endUnmatched(request, response);
489
- }
490
- } catch (err) {
491
- // an internal throw answers 500 as express's final handler would, instead of
492
- // dying as an unhandled rejection
493
- if (response.aborted || response.finished) {
494
- console.error(err);
495
- } else {
496
- this._handleError(err, null, request, response);
497
- }
492
+ this.uwsApp.any("/*", (res, req) => this._serveGeneric(res, req));
493
+ }
494
+
495
+ /**
496
+ * Serves one request by walking this app's chain, with no registration-time shortcut. It is
497
+ * what the catch-all runs, and also what a native registration falls back to when it sees a
498
+ * request it must not answer itself, see the case guard in Router#_registerUwsRoute.
499
+ *
500
+ * @param {any} res the uWS response
501
+ * @param {any} req the uWS request
502
+ */
503
+ async _serveGeneric(res, req) {
504
+ const request = this.handleRequest(res, req);
505
+ const response = request.res;
506
+ // armed up front here: this handler awaits, so the response outlives the callback
507
+ // on every path through it
508
+ this._armAbort(res, response);
509
+
510
+ try {
511
+ const routed = this._routeRequest(request, response);
512
+ // dispatch has run its synchronous stretch inside _routeRequest by now, still
513
+ // under the cork uWS holds for this callback; the await below leaves it
514
+ response._corkNeeded = true;
515
+ const matchedRoute = await routed;
516
+ if (!matchedRoute && !response.headersSent && !response.aborted) {
517
+ this._endUnmatched(request, response);
498
518
  }
499
- });
519
+ } catch (err) {
520
+ // an internal throw answers 500 as express's final handler would, instead of
521
+ // dying as an unhandled rejection
522
+ if (response.aborted || response.finished) {
523
+ console.error(err);
524
+ } else {
525
+ this._handleError(err, null, request, response);
526
+ }
527
+ }
500
528
  }
501
529
 
502
530
  /**
@@ -733,7 +761,12 @@ class Application extends Router {
733
761
  view = new View(name, {
734
762
  defaultEngine: this.get("view engine"),
735
763
  root: this.get("views"),
736
- engines: { ...this.engines }
764
+ // the object itself, not a copy of it: a mounted app reaches its parent's engines
765
+ // through the prototype chain, and a spread only carries what the app owns, so a
766
+ // sub-app rendering with the parent's engine went off to require() a module named
767
+ // after the extension. Express hands its own object over too, and means to: a view
768
+ // that loads an engine by require caches it back here
769
+ engines: this.engines
737
770
  });
738
771
  if (!view.path) {
739
772
  const dirs =