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.
@@ -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
  [![npm version](https://img.shields.io/npm/v/fulmine.js)](https://www.npmjs.com/package/fulmine.js)
36
30
  [![Node.js >= 22.0.0](https://img.shields.io/badge/Node.js-%3E=22.0.0-green)](https://nodejs.org)
37
31
  [![Coverage Status](https://coveralls.io/repos/github/nigrosimone/fulmine.js/badge.svg?branch=main)](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)** (the link lands filtered on the JavaScript entries): first among the JavaScript entries, across fifteen subscribed profiles. The saved runs measure 24.3 million WebSocket echoes per second pipelined and 3.77 million one-at-a-time (past Bun's own dedicated WebSocket entry), 7.3 million pipelined HTTP requests per second, 1.12 million on the json profile with 1.04 million of that surviving TLS, 457 thousand on compressed json, and 359 thousand on the Postgres CRUD profile, within ten percent of the leading Rust and C# entries there.
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
- Two things about µWebSockets.js make a Dockerfile that works for Express fail here, and both have easy answers:
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
- The clean way to satisfy both is a multi-stage build: install with the full image, run with the slim one.
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. 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.
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
- 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.
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
- 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.
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
- 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.
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.5.1",
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",
@@ -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
- async _serveGeneric(res, req) {
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 awaits, so the response outlives the callback
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
- 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);
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
- const take = (node) => found.push({ start: node.getStart(sourceFile), end: node.getEnd() });
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) && arg.text === FROM) {
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 === FROM
255
+ typeof node.source?.value === "string"
230
256
  ) {
231
- found.push({ start: node.source.start, end: node.source.end });
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 === FROM) {
241
- found.push({ start: arg.start, end: arg.end });
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 specifier at all
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 ? findSpecifiersTypeScript(source, file, ts) : findSpecifiers(source);
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();