fulmine.js 5.5.1 → 5.6.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/COMPRESSION_LICENSE +25 -0
- package/NOTICE +9 -0
- package/README.md +29 -18
- package/package.json +4 -2
- package/src/application.js +6 -21
- package/src/cli.js +57 -15
- package/src/compression.js +400 -0
- package/src/index.js +4 -0
- package/src/middlewares.js +82 -1
- package/src/options.d.ts +6 -0
- package/src/request.js +0 -4
- package/src/response.js +30 -36
- package/src/router.js +26 -3
- package/src/types.d.ts +19 -0
- package/src/utils.js +116 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
compression License (only code derived from the compression module).
|
|
2
|
+
|
|
3
|
+
(The MIT License)
|
|
4
|
+
|
|
5
|
+
Copyright (c) 2014 Jonathan Ong <me@jongleberry.com>
|
|
6
|
+
Copyright (c) 2014-2015 Douglas Christopher Wilson <doug@somethingdoug.com>
|
|
7
|
+
|
|
8
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
9
|
+
a copy of this software and associated documentation files (the
|
|
10
|
+
'Software'), to deal in the Software without restriction, including
|
|
11
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
12
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
13
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
14
|
+
the following conditions:
|
|
15
|
+
|
|
16
|
+
The above copyright notice and this permission notice shall be
|
|
17
|
+
included in all copies or substantial portions of the Software.
|
|
18
|
+
|
|
19
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
|
20
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
21
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
22
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
23
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
24
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
25
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/NOTICE
CHANGED
|
@@ -43,6 +43,15 @@ every copy:
|
|
|
43
43
|
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
44
44
|
DEALINGS IN THE SOFTWARE.
|
|
45
45
|
|
|
46
|
+
This product includes test code derived from the compression middleware
|
|
47
|
+
(https://github.com/expressjs/compression), ported in
|
|
48
|
+
tests/unit/compression.test.js and licensed under the MIT License. Its terms
|
|
49
|
+
travel with this repository in COMPRESSION_LICENSE, and the copyright it
|
|
50
|
+
requires to be carried is:
|
|
51
|
+
|
|
52
|
+
Copyright (c) 2014 Jonathan Ong <me@jongleberry.com>
|
|
53
|
+
Copyright (c) 2014-2015 Douglas Christopher Wilson <doug@somethingdoug.com>
|
|
54
|
+
|
|
46
55
|
As required by section 4(b) of the Apache License, the following are the
|
|
47
56
|
significant changes made to the original work:
|
|
48
57
|
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<img src="./assets/logo-mark.svg" alt="" width="88" align="right">
|
|
2
2
|
|
|
3
|
-
# Fulmine
|
|
3
|
+
# Fulmine.js
|
|
4
4
|
|
|
5
5
|
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.
|
|
6
6
|
|
|
@@ -26,12 +26,6 @@ npx fulmine profile # what listen() decided about each route
|
|
|
26
26
|
|
|
27
27
|
See [Migrating](#migrating) for what it handles and what it deliberately does not.
|
|
28
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
|
-
|
|
35
29
|
[](https://www.npmjs.com/package/fulmine.js)
|
|
36
30
|
[](https://nodejs.org)
|
|
37
31
|
[](https://coveralls.io/github/nigrosimone/fulmine.js?branch=main)
|
|
@@ -65,7 +59,6 @@ shared machine, so the number would describe the machine rather than the framewo
|
|
|
65
59
|
- [Router](#router)
|
|
66
60
|
- [Tested middlewares](#tested-middlewares)
|
|
67
61
|
- [Tested view engines](#tested-view-engines)
|
|
68
|
-
- [The demo](https://fulmine-demo.fly.dev)
|
|
69
62
|
- [Working on Fulmine](./CONTRIBUTING.md)
|
|
70
63
|
|
|
71
64
|
## Why this exists
|
|
@@ -96,7 +89,7 @@ to run it yourself.
|
|
|
96
89
|
|
|
97
90
|
Numbers produced by a project about itself deserve suspicion, so Fulmine also stands in public arenas, run by their own rigs under their own rules:
|
|
98
91
|
|
|
99
|
-
- **[HttpArena](https://www.http-arena.com/#sort=rps:-1&q=Js)
|
|
92
|
+
- **[HttpArena](https://www.http-arena.com/#sort=rps:-1&q=Js)**: thirty profiles on 64-core dedicated hardware, same conditions for every entry, rerun whenever one of them changes. The link lands filtered on the JavaScript entries. No figures are copied here on purpose: the board is the current one and this page would not be.
|
|
100
93
|
- **[web-frameworks](https://web-frameworks-benchmark.netlify.app/result?l=javascript)**: entry merged, numbers arrive with their next published round.
|
|
101
94
|
|
|
102
95
|
More to come as their maintainers take the entries in.
|
|
@@ -131,6 +124,10 @@ npx fulmine differences # print the list below and change nothing
|
|
|
131
124
|
The command is installed under both `fulmine` and `fulmine.js`. Use `fulmine`: `npx` cannot run a
|
|
132
125
|
command whose name ends in `.js` on Windows, where it exits without a word.
|
|
133
126
|
|
|
127
|
+
It also names the middlewares it found that have a faster one built in here, `compression`,
|
|
128
|
+
`body-parser` and `serve-static`, and leaves them to you: the replacement is reached through the
|
|
129
|
+
`express` import, and no rewrite can know that it is in scope where they are required.
|
|
130
|
+
|
|
134
131
|
### Angular SSR
|
|
135
132
|
|
|
136
133
|
The `server.ts` that `ng add @angular/ssr` generates is an ordinary Express application, so the same
|
|
@@ -202,12 +199,17 @@ Bun is not an option: µWebSockets.js is a native Node addon, and Bun does not l
|
|
|
202
199
|
|
|
203
200
|
## Docker
|
|
204
201
|
|
|
205
|
-
|
|
202
|
+
Three things about µWebSockets.js make a Dockerfile that works for Express fail here, and all three have easy answers:
|
|
206
203
|
|
|
207
204
|
- **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.
|
|
208
205
|
- **`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.
|
|
206
|
+
- **git must be allowed to speak https.** Where the build environment rewrites GitHub URLs to ssh, which some CI images and company-wide git configs do, the fetch asks for a key the image does not have and the install dies on a permission denied that never names µWebSockets.js. One line before `npm ci` puts it back:
|
|
209
207
|
|
|
210
|
-
|
|
208
|
+
```dockerfile
|
|
209
|
+
RUN git config --global url."https://github.com/".insteadOf "ssh://git@github.com/"
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
The clean way to satisfy the first two is a multi-stage build: install with the full image, run with the slim one.
|
|
211
213
|
|
|
212
214
|
```dockerfile
|
|
213
215
|
FROM node:26-trixie AS build
|
|
@@ -230,6 +232,7 @@ A single-stage `node:26-trixie-slim` image works too if you `apt-get install -y
|
|
|
230
232
|
- `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.
|
|
231
233
|
- `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.
|
|
232
234
|
- 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.
|
|
235
|
+
- **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`.
|
|
233
236
|
- For HTTPS, instead of doing this:
|
|
234
237
|
|
|
235
238
|
```js
|
|
@@ -371,17 +374,24 @@ Worth changing, if these are routes that carry traffic
|
|
|
371
374
|
|
|
372
375
|
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.
|
|
373
376
|
|
|
374
|
-
2. Do not use external `serve-static` module. Instead use built-in `express.static()` middleware, which is optimized for Fulmine.
|
|
377
|
+
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. It costs one more `stat` per request and sends a fraction of the bytes: on a 4KB script with a brotli twin, 12 times fewer. `Vary: Accept-Encoding` is sent whether or not a variant is found, the content type stays the one the requested name implies, and each variant carries its own ETag.
|
|
375
378
|
|
|
376
379
|
3. Do not use `body-parser` module. Instead use built-in `express.text()`, `express.json()` etc.
|
|
377
380
|
|
|
378
|
-
4.
|
|
381
|
+
4. Do not use the `compression` module. `express.compression()` takes the same options and decides the same way, and it served about 50% more requests per second on an 8KB JSON body here, gzip and brotli alike. A response that arrives whole, which is every `res.send()` and `res.json()`, is compressed in one call rather than through a transform stream and goes out with a `Content-Length` instead of chunked; a response written in pieces still streams. The bytes are the same bytes either way.
|
|
382
|
+
|
|
383
|
+
```js
|
|
384
|
+
// the compression module's options, unchanged: threshold, filter, level, brotli, enforceEncoding
|
|
385
|
+
app.use(express.compression({ threshold: 1024 }));
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
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.
|
|
379
389
|
|
|
380
|
-
|
|
390
|
+
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.
|
|
381
391
|
|
|
382
|
-
|
|
392
|
+
7. `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.
|
|
383
393
|
|
|
384
|
-
|
|
394
|
+
8. 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.
|
|
385
395
|
|
|
386
396
|
## WebSockets
|
|
387
397
|
|
|
@@ -511,6 +521,7 @@ In general, basically all features and options are supported. Use the [Express 5
|
|
|
511
521
|
- ✅ express.static()
|
|
512
522
|
- ✅ express.text()
|
|
513
523
|
- ✅ express.raw()
|
|
524
|
+
- ✅ express.compression(). Fulmine's own, since Express has none: it is the [compression](https://npmjs.com/package/compression) module's options and behaviour built in, described under [Performance tips](#performance-tips).
|
|
514
525
|
- 🚧 express.request (this is not a constructor but a prototype for replacing methods)
|
|
515
526
|
- 🚧 express.response (this is not a constructor but a prototype for replacing methods)
|
|
516
527
|
- 🚧 express.application (likewise: a method added here is on every app)
|
|
@@ -569,7 +580,7 @@ Two of these keep a compiled form alongside the value, which you can also set di
|
|
|
569
580
|
Fulmine adds three of its own:
|
|
570
581
|
|
|
571
582
|
- `declarative responses`, on by default. Lets a simple enough handler be compiled into a native uWS response, described under [Performance tips](#performance-tips).
|
|
572
|
-
- `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.
|
|
583
|
+
- `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. Turn it off where every request has to reach the disk, which is what a public benchmark asks of a standard entry: it was worth about 4% on a 4KB file here, so the cost of turning it off is small.
|
|
573
584
|
- `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.
|
|
574
585
|
|
|
575
586
|
### Request
|
|
@@ -677,7 +688,7 @@ Almost all middlewares that are compatible with Express are compatible with Fulm
|
|
|
677
688
|
- ✅ [body-parser](https://npmjs.com/package/body-parser) (use `express.text()` etc instead for better performance)
|
|
678
689
|
- ✅ [cookie-parser](https://npmjs.com/package/cookie-parser)
|
|
679
690
|
- ✅ [cookie-session](https://npmjs.com/package/cookie-session)
|
|
680
|
-
- ✅ [compression](https://npmjs.com/package/compression)
|
|
691
|
+
- ✅ [compression](https://npmjs.com/package/compression) (use `express.compression()` instead for better performance)
|
|
681
692
|
- ✅ [serve-static](https://npmjs.com/package/serve-static) (use `express.static()` instead for better performance)
|
|
682
693
|
- ✅ [serve-index](https://npmjs.com/package/serve-index)
|
|
683
694
|
- ✅ [cors](https://npmjs.com/package/cors)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fulmine.js",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.6.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": {
|
|
@@ -40,7 +40,8 @@
|
|
|
40
40
|
"src",
|
|
41
41
|
"LICENSE",
|
|
42
42
|
"NOTICE",
|
|
43
|
-
"EXPRESS_LICENSE"
|
|
43
|
+
"EXPRESS_LICENSE",
|
|
44
|
+
"COMPRESSION_LICENSE"
|
|
44
45
|
],
|
|
45
46
|
"repository": {
|
|
46
47
|
"type": "git",
|
|
@@ -72,6 +73,7 @@
|
|
|
72
73
|
"accepts": "^2.0.0",
|
|
73
74
|
"acorn": "^8.16.0",
|
|
74
75
|
"bytes": "^3.1.2",
|
|
76
|
+
"compressible": "^2.0.18",
|
|
75
77
|
"content-disposition": "^1.1.0",
|
|
76
78
|
"cookie": "^1.1.1",
|
|
77
79
|
"cookie-signature": "^1.2.2",
|
package/src/application.js
CHANGED
|
@@ -500,31 +500,16 @@ class Application extends Router {
|
|
|
500
500
|
* @param {any} res the uWS response
|
|
501
501
|
* @param {any} req the uWS request
|
|
502
502
|
*/
|
|
503
|
-
|
|
503
|
+
_serveGeneric(res, req) {
|
|
504
504
|
const request = this.handleRequest(res, req);
|
|
505
505
|
const response = request.res;
|
|
506
|
-
// armed up front here: this handler
|
|
507
|
-
// on every path through it
|
|
506
|
+
// armed up front here: this handler can outlive the callback on every path through it
|
|
508
507
|
this._armAbort(res, response);
|
|
509
508
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
response._corkNeeded = true;
|
|
515
|
-
const matchedRoute = await routed;
|
|
516
|
-
if (!matchedRoute && !response.headersSent && !response.aborted) {
|
|
517
|
-
this._endUnmatched(request, response);
|
|
518
|
-
}
|
|
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
|
-
}
|
|
509
|
+
this._routeRequestDirect(request, response);
|
|
510
|
+
// the synchronous stretch has run under the cork uWS holds for this callback, and
|
|
511
|
+
// whatever comes after it is outside
|
|
512
|
+
response._corkNeeded = true;
|
|
528
513
|
}
|
|
529
514
|
|
|
530
515
|
/**
|
package/src/cli.js
CHANGED
|
@@ -34,6 +34,16 @@ const acorn = require("acorn");
|
|
|
34
34
|
const FROM = "express";
|
|
35
35
|
const TO = "fulmine.js";
|
|
36
36
|
|
|
37
|
+
// Modules this has a faster version of, spotted while the files are being read anyway. They are
|
|
38
|
+
// reported and not rewritten: the replacement is reached through the express import, which this
|
|
39
|
+
// command cannot know is in scope in the file that requires them, and body-parser is four
|
|
40
|
+
// functions rather than one.
|
|
41
|
+
const BUILT_IN_INSTEAD = {
|
|
42
|
+
compression: "express.compression(), which takes the same options",
|
|
43
|
+
"serve-static": "express.static()",
|
|
44
|
+
"body-parser": "express.json(), express.urlencoded(), express.text(), express.raw()"
|
|
45
|
+
};
|
|
46
|
+
|
|
37
47
|
const SKIP_DIRS = new Set(["node_modules", ".git", "dist", "build", "coverage", ".nyc_output", ".next"]);
|
|
38
48
|
const EXTENSIONS = new Set([".js", ".mjs", ".cjs", ".ts", ".mts", ".cts", ".tsx"]);
|
|
39
49
|
const TYPESCRIPT_EXTENSIONS = new Set([".ts", ".mts", ".cts", ".tsx"]);
|
|
@@ -139,9 +149,10 @@ function loadTypeScript(target) {
|
|
|
139
149
|
* @param {string} source
|
|
140
150
|
* @param {string} fileName decides whether JSX is allowed, so a .tsx angle bracket is not a cast
|
|
141
151
|
* @param {any} ts the compiler
|
|
152
|
+
* @param {Set<string>} [seen] as in findSpecifiers
|
|
142
153
|
* @returns {{start: number, end: number}[]}
|
|
143
154
|
*/
|
|
144
|
-
function findSpecifiersTypeScript(source, fileName, ts) {
|
|
155
|
+
function findSpecifiersTypeScript(source, fileName, ts, seen) {
|
|
145
156
|
const sourceFile = ts.createSourceFile(
|
|
146
157
|
fileName,
|
|
147
158
|
source,
|
|
@@ -152,7 +163,14 @@ function findSpecifiersTypeScript(source, fileName, ts) {
|
|
|
152
163
|
|
|
153
164
|
/** @type {{start: number, end: number}[]} */
|
|
154
165
|
const found = [];
|
|
155
|
-
|
|
166
|
+
/** @param {any} node a string literal naming a module */
|
|
167
|
+
const take = (node) => {
|
|
168
|
+
if (node.text === FROM) {
|
|
169
|
+
found.push({ start: node.getStart(sourceFile), end: node.getEnd() });
|
|
170
|
+
} else if (seen && BUILT_IN_INSTEAD[node.text]) {
|
|
171
|
+
seen.add(node.text);
|
|
172
|
+
}
|
|
173
|
+
};
|
|
156
174
|
|
|
157
175
|
const visit = (node) => {
|
|
158
176
|
// import express from "express", import type { Request } from "express", export * from it.
|
|
@@ -160,23 +178,21 @@ function findSpecifiersTypeScript(source, fileName, ts) {
|
|
|
160
178
|
if (
|
|
161
179
|
(ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) &&
|
|
162
180
|
node.moduleSpecifier &&
|
|
163
|
-
ts.isStringLiteral(node.moduleSpecifier)
|
|
164
|
-
node.moduleSpecifier.text === FROM
|
|
181
|
+
ts.isStringLiteral(node.moduleSpecifier)
|
|
165
182
|
) {
|
|
166
183
|
take(node.moduleSpecifier);
|
|
167
184
|
} else if (
|
|
168
185
|
// import express = require("express"), which is TypeScript's own spelling
|
|
169
186
|
ts.isImportEqualsDeclaration(node) &&
|
|
170
187
|
ts.isExternalModuleReference(node.moduleReference) &&
|
|
171
|
-
ts.isStringLiteral(node.moduleReference.expression)
|
|
172
|
-
node.moduleReference.expression.text === FROM
|
|
188
|
+
ts.isStringLiteral(node.moduleReference.expression)
|
|
173
189
|
) {
|
|
174
190
|
take(node.moduleReference.expression);
|
|
175
191
|
} else if (ts.isCallExpression(node)) {
|
|
176
192
|
const isRequire = ts.isIdentifier(node.expression) && node.expression.text === "require";
|
|
177
193
|
const isDynamicImport = node.expression.kind === ts.SyntaxKind.ImportKeyword;
|
|
178
194
|
const arg = node.arguments[0];
|
|
179
|
-
if ((isRequire || isDynamicImport) && arg && ts.isStringLiteral(arg)
|
|
195
|
+
if ((isRequire || isDynamicImport) && arg && ts.isStringLiteral(arg)) {
|
|
180
196
|
take(arg);
|
|
181
197
|
}
|
|
182
198
|
}
|
|
@@ -193,9 +209,11 @@ function findSpecifiersTypeScript(source, fileName, ts) {
|
|
|
193
209
|
* imports at all, and none of those may be rewritten.
|
|
194
210
|
*
|
|
195
211
|
* @param {string} source
|
|
212
|
+
* @param {Set<string>} [seen] collects the names of the modules with something built in here,
|
|
213
|
+
* which are recognised on the same walk rather than on one of their own
|
|
196
214
|
* @returns {{start: number, end: number}[]|null} null when the file does not parse
|
|
197
215
|
*/
|
|
198
|
-
function findSpecifiers(source) {
|
|
216
|
+
function findSpecifiers(source, seen) {
|
|
199
217
|
/** @type {any} */
|
|
200
218
|
let tree;
|
|
201
219
|
// A file is either a module or a script and the parser has to be told which. Try module first,
|
|
@@ -220,15 +238,23 @@ function findSpecifiers(source) {
|
|
|
220
238
|
|
|
221
239
|
/** @type {{start: number, end: number}[]} */
|
|
222
240
|
const found = [];
|
|
241
|
+
/** @param {any} node a string literal naming a module */
|
|
242
|
+
const record = (node) => {
|
|
243
|
+
if (node.value === FROM) {
|
|
244
|
+
found.push({ start: node.start, end: node.end });
|
|
245
|
+
} else if (seen && BUILT_IN_INSTEAD[node.value]) {
|
|
246
|
+
seen.add(node.value);
|
|
247
|
+
}
|
|
248
|
+
};
|
|
223
249
|
walk(tree, (node) => {
|
|
224
250
|
// import express from "express", export * from "express"
|
|
225
251
|
if (
|
|
226
252
|
(node.type === "ImportDeclaration" ||
|
|
227
253
|
node.type === "ExportNamedDeclaration" ||
|
|
228
254
|
node.type === "ExportAllDeclaration") &&
|
|
229
|
-
node.source?.value ===
|
|
255
|
+
typeof node.source?.value === "string"
|
|
230
256
|
) {
|
|
231
|
-
|
|
257
|
+
record(node.source);
|
|
232
258
|
return;
|
|
233
259
|
}
|
|
234
260
|
// require("express") and import("express"), the second being a node of its own
|
|
@@ -237,8 +263,8 @@ function findSpecifiers(source) {
|
|
|
237
263
|
const isDynamicImport = node.type === "ImportExpression";
|
|
238
264
|
if (isRequire || isDynamicImport) {
|
|
239
265
|
const arg = isDynamicImport ? node.source : node.arguments?.[0];
|
|
240
|
-
if (arg?.type === "Literal" && arg.value ===
|
|
241
|
-
|
|
266
|
+
if (arg?.type === "Literal" && typeof arg.value === "string") {
|
|
267
|
+
record(arg);
|
|
242
268
|
}
|
|
243
269
|
}
|
|
244
270
|
});
|
|
@@ -582,6 +608,8 @@ Options:
|
|
|
582
608
|
const unparsed = [];
|
|
583
609
|
/** @type {string[]} */
|
|
584
610
|
const needTypeScript = [];
|
|
611
|
+
/** @type {Set<string>} */
|
|
612
|
+
const builtInInstead = new Set();
|
|
585
613
|
|
|
586
614
|
// resolved once, and only if there is anything to use it on
|
|
587
615
|
const hasTypeScriptFiles = files.some((file) => TYPESCRIPT_EXTENSIONS.has(path.extname(file)));
|
|
@@ -590,8 +618,8 @@ Options:
|
|
|
590
618
|
for (const file of files) {
|
|
591
619
|
const source = fs.readFileSync(file, "utf8");
|
|
592
620
|
// reading every file's AST to find nothing is the common case, so skip the ones that
|
|
593
|
-
// cannot contain the
|
|
594
|
-
if (!source.includes(FROM)) continue;
|
|
621
|
+
// cannot contain any of the names being looked for
|
|
622
|
+
if (!source.includes(FROM) && !Object.keys(BUILT_IN_INSTEAD).some((name) => source.includes(name))) continue;
|
|
595
623
|
|
|
596
624
|
const isTypeScript = TYPESCRIPT_EXTENSIONS.has(path.extname(file));
|
|
597
625
|
if (isTypeScript && !ts) {
|
|
@@ -599,7 +627,9 @@ Options:
|
|
|
599
627
|
continue;
|
|
600
628
|
}
|
|
601
629
|
|
|
602
|
-
const specifiers = isTypeScript
|
|
630
|
+
const specifiers = isTypeScript
|
|
631
|
+
? findSpecifiersTypeScript(source, file, ts, builtInInstead)
|
|
632
|
+
: findSpecifiers(source, builtInInstead);
|
|
603
633
|
if (specifiers === null) {
|
|
604
634
|
unparsed.push(path.relative(target, file));
|
|
605
635
|
continue;
|
|
@@ -635,6 +665,18 @@ Options:
|
|
|
635
665
|
console.log(
|
|
636
666
|
`\n${dryRun ? "would rewrite" : "rewrote"} ${changedImports} import(s) in ${changedFiles} file(s) of ${files.length} scanned`
|
|
637
667
|
);
|
|
668
|
+
|
|
669
|
+
// said whether or not anything was rewritten: an application migrated last month still has
|
|
670
|
+
// these, and they are the difference between running on µWS and running through a middleware
|
|
671
|
+
// that was written for node streams
|
|
672
|
+
if (builtInInstead.size) {
|
|
673
|
+
console.log(`\n${builtInInstead.size} module(s) with a faster one built in here, worth replacing by hand:`);
|
|
674
|
+
for (const name of builtInInstead) {
|
|
675
|
+
console.log(` ${name} -> ${BUILT_IN_INSTEAD[name]}`);
|
|
676
|
+
}
|
|
677
|
+
console.log("");
|
|
678
|
+
}
|
|
679
|
+
|
|
638
680
|
if (changedFiles) {
|
|
639
681
|
console.log(`Remember to install it: npm install ${TO}`);
|
|
640
682
|
printDifferences();
|