fulmine.js 5.19.2 → 5.19.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fulmine.js",
3
- "version": "5.19.2",
3
+ "version": "5.19.4",
4
4
  "description": "Drop-in Express 5 replacement on uWebSockets.js. Your existing middleware keeps working.",
5
5
  "main": "src/index.js",
6
6
  "exports": {
@@ -149,6 +149,7 @@
149
149
  "eslint": "^10.8.0",
150
150
  "eslint-config-prettier": "^10.1.8",
151
151
  "eslint-plugin-jsdoc": "^64.1.0",
152
+ "eslint-plugin-sonarjs": "^4.2.0",
152
153
  "etag": "^1.8.1",
153
154
  "eventsource": "^5.0.0",
154
155
  "exit-hook": "^5.1.0",
package/src/adopt.js CHANGED
@@ -17,15 +17,14 @@ limitations under the License.
17
17
  // The two commands that edit a config file rather than source: `npx fulmine.js override` and
18
18
  // `npx fulmine.js angular`.
19
19
  //
20
- // `migrate` rewrites `require("express")` in your own files, which is all an application needs. The
21
- // two cases it cannot reach are both a line in a JSON file that nobody remembers the shape of:
20
+ // `migrate` rewrites `require("express")` in your own files. The two cases it cannot reach are
21
+ // both a line in a JSON file:
22
22
  //
23
- // override A framework built on Express does not require it in your code, it requires it in its
24
- // own, so there is no specifier to rewrite. Every package manager can answer `express`
25
- // with this package instead, for the whole tree, and each one spells it differently.
23
+ // override A framework built on Express requires it in its own code, so there is no specifier
24
+ // to rewrite. Every package manager can answer `express` with this package instead,
25
+ // and each one spells it differently.
26
26
  // angular An Angular server bundle is built with esbuild, which inlines every dependency and
27
- // cannot load µWS's native binary. Two names in `externalDependencies` fix it, and
28
- // nothing in the error message it fails with says so.
27
+ // cannot load uWS's native binary. Two names in `externalDependencies` fix it.
29
28
 
30
29
  "use strict";
31
30
 
@@ -72,9 +71,9 @@ function indentOf(source) {
72
71
  /**
73
72
  * Reads a JSON file, or explains why it could not be read rather than throwing a parser error.
74
73
  *
75
- * The read is attempted rather than guarded by an existence check. Both commands go on to write the
76
- * file they read, and a check on a path followed by a write to the same path is the shape of a race
77
- * whatever the odds of losing it. `code` is what a caller names the missing file by.
74
+ * The read is attempted rather than guarded by an existence check: both commands write the file
75
+ * they read, and a check followed by a write to the same path is a race. `code` is what a caller
76
+ * names the missing file by.
78
77
  *
79
78
  * @param {string} file
80
79
  * @returns {{data: any, source: string}|{error: string, code: string|undefined}}
@@ -121,7 +120,7 @@ function detectManager(dir, pkg) {
121
120
  /**
122
121
  * Reads a nested key, and answers undefined rather than throwing on a missing level.
123
122
  *
124
- * @param {any} object
123
+ * @param {any} object parsed JSON, so its shape is whatever the file held
125
124
  * @param {string[]} keys
126
125
  * @returns {any}
127
126
  */
@@ -137,9 +136,9 @@ function readPath(object, keys) {
137
136
  /**
138
137
  * Writes a nested key, making the levels above it as it goes.
139
138
  *
140
- * @param {any} object
139
+ * @param {any} object parsed JSON, so its shape is whatever the file held
141
140
  * @param {string[]} keys
142
- * @param {any} value
141
+ * @param {any} value whatever belongs at that key
143
142
  * @returns {void}
144
143
  */
145
144
  function writePath(object, keys, value) {
@@ -155,9 +154,7 @@ function writePath(object, keys, value) {
155
154
  * npx fulmine.js override [dir] [--dry-run]
156
155
  *
157
156
  * Puts the substitution in package.json where this project's package manager reads it, and says
158
- * what to run next. It deliberately does not run the install: the reinstall throws away
159
- * node_modules, and that is not something a command should do to somebody's working tree without
160
- * being watched.
157
+ * what to run next. It does not run the install itself: that throws away node_modules.
161
158
  *
162
159
  * @param {string[]} argv everything after the command name
163
160
  * @returns {number} exit code
@@ -235,9 +232,8 @@ function override(argv) {
235
232
  /**
236
233
  * Every build target in an angular.json that produces a server bundle.
237
234
  *
238
- * A browser-only build has nothing to declare external: the bundle it makes never loads µWS. What
239
- * marks a server build is `ssr`, `server` or `outputMode` in its options, which is what `ng add
240
- * @angular/ssr` writes.
235
+ * A browser-only build never loads uWS. What marks a server build is `ssr`, `server` or
236
+ * `outputMode` in its options, which is what `ng add @angular/ssr` writes.
241
237
  *
242
238
  * @param {any} config the parsed angular.json
243
239
  * @returns {{name: string, options: any}[]}
@@ -257,8 +253,8 @@ function serverBuilds(config) {
257
253
  /**
258
254
  * npx fulmine.js angular [dir] [--dry-run]
259
255
  *
260
- * Declares this package and µWebSockets.js external in every server build, which is what stops
261
- * esbuild trying to inline a native binary it cannot read.
256
+ * Declares this package and uWebSockets.js external in every server build, which stops esbuild
257
+ * trying to inline a native binary it cannot read.
262
258
  *
263
259
  * @param {string[]} argv everything after the command name
264
260
  * @returns {number} exit code
@@ -267,11 +263,9 @@ function angular(argv) {
267
263
  const dryRun = argv.includes("--dry-run");
268
264
  const given = path.resolve(argv.find((arg) => !arg.startsWith("--")) ?? ".");
269
265
 
270
- // The argument is either the file or the directory holding it, and which one comes out of
271
- // reading it rather than out of a stat: a directory reads as EISDIR and a missing path as
272
- // ENOENT, and in both cases the file to look for is angular.json inside it. Asked this way
273
- // round because this function goes on to write the file it read, and a check on a path followed
274
- // by a write to the same path is the shape of a race whatever the odds of losing it.
266
+ // The argument is either the file or the directory holding it, told apart by reading it and not
267
+ // by a stat: a directory reads EISDIR, a missing path ENOENT, and in both cases the file to
268
+ // look for is angular.json inside it. A stat followed by a write to the same path is a race.
275
269
  let file = given;
276
270
  let read = readJson(file);
277
271
  if ("error" in read && (read.code === "EISDIR" || read.code === "ENOENT")) {
@@ -103,6 +103,13 @@ class Application extends Router {
103
103
  */
104
104
  _isApplication = true;
105
105
 
106
+ /**
107
+ * Whether express.testing already compiled the routes of this app. Written there and nowhere
108
+ * else: a second compilation would register everything with uWS twice. See src/testing.js.
109
+ * @type {boolean|undefined}
110
+ */
111
+ _testingCompiled;
112
+
106
113
  /**
107
114
  * @param {object} [settings] the options express() takes. uwsOptions goes to uWS and decides
108
115
  * between an HTTP, an HTTPS and an HTTP/3 server; threads sizes the file-reading pool, and 0
@@ -154,11 +161,13 @@ class Application extends Router {
154
161
  // was an allocation on every request
155
162
  this._request = class extends Request {
156
163
  /**
157
- * @param {any} req
158
- * @param {any} res
159
- * @param {any} app
160
- * @param {any} [preset]
161
- * @param {any} [skipHolder]
164
+ * The base constructor's arguments, written out rather than spread. See Request.
165
+ *
166
+ * @param {import("uWebSockets.js").HttpRequest} req uWS request
167
+ * @param {import("uWebSockets.js").HttpResponse} res uWS response
168
+ * @param {Application} app the application this request arrived at
169
+ * @param {import("./router-utils.js").NativePreset} [preset] a literal registration's constants
170
+ * @param {import("./router-utils.js").SkipHolder} [skipHolder] where a granted header skip lives
162
171
  */
163
172
  constructor(req, res, app, preset, skipHolder) {
164
173
  super(req, res, app, preset, skipHolder);
@@ -166,27 +175,15 @@ class Application extends Router {
166
175
  };
167
176
  this._response = class extends Response {
168
177
  /**
169
- * @param {any} res
170
- * @param {any} req
171
- * @param {any} app
178
+ * The base constructor's arguments, written out rather than spread. See Response.
179
+ *
180
+ * @param {import("uWebSockets.js").HttpResponse} res uWS response
181
+ * @param {Request} req the Request, already built
182
+ * @param {Application} app the application this request arrived at
172
183
  */
173
184
  constructor(res, req, app) {
174
185
  super(res, req, app);
175
186
  }
176
-
177
- /**
178
- * Node counts an explicit writeHead as the head gone out; remembered here so the
179
- * automatic OPTIONS reply can refuse to add headers after it, as express's does.
180
- *
181
- * @param {number} statusCode
182
- * @param {string|Record<string, any>} [statusMessage]
183
- * @param {Record<string, any>} [headers]
184
- * @returns {this}
185
- */
186
- writeHead(statusCode, statusMessage, headers) {
187
- this._headWritten = true;
188
- return super.writeHead(statusCode, statusMessage, headers);
189
- }
190
187
  };
191
188
  this.request = this._request.prototype;
192
189
  this.response = this._response.prototype;
@@ -242,9 +239,9 @@ class Application extends Router {
242
239
  // stores where a Set paid identity hashing and table upkeep per request. A holder object
243
240
  // rather than a bare field, because the callable app copies own scalars by value and two
244
241
  // copies of a head would disagree; an object rides by reference, the way the Set did
245
- this._pending = /** @type {{ head: any }} */ ({ head: null });
242
+ this._pending = /** @type {{ head: Response|null }} */ ({ head: null });
246
243
  // on the per-app prototype layer, not per response, same as the Set was
247
- /** @type {any} */ (this.response)._pendingIn = this._pending;
244
+ /** @type {{_pendingIn?: {head: Response|null}}} */ (this.response)._pendingIn = this._pending;
248
245
  this._draining = false;
249
246
  // read here, at construction, the way express does; an empty NODE_ENV means development,
250
247
  // which the ?? in the shared default would miss
@@ -274,8 +271,8 @@ class Application extends Router {
274
271
  * message carries data and not closures. The counter wraps rather than growing without bound,
275
272
  * a million tasks being far more than can be outstanding at once.
276
273
  *
277
- * @param {(value: any) => void} resolve
278
- * @param {(err: any) => void} reject
274
+ * @param {(value: Buffer) => void} resolve
275
+ * @param {(err: Error) => void} reject
279
276
  * @returns {number} the key to send to the worker
280
277
  */
281
278
  createWorkerTask(resolve, reject) {
@@ -306,11 +303,10 @@ class Application extends Router {
306
303
 
307
304
  /**
308
305
  * A small file through the worker pool, with two things on top: concurrent asks for the same
309
- * path share one read, and the bytes of an unchanged file come from a bounded cache,
310
- * validated against the stat the caller already paid for, so a touched file is re-read.
311
- * A hit completes on a macrotask, which is when a worker's answer would have arrived; code
312
- * that passed the suites against worker timing keeps passing against this.
313
- * `app.set("file cache", false)` turns the cache off; the shared read stays.
306
+ * path share one read, and the bytes of an unchanged file come from a bounded cache, validated
307
+ * against the stat the caller already paid for, so a touched file is re-read. A hit completes
308
+ * on a macrotask, which is when a worker's answer would have arrived.
309
+ * `app.set("file cache", false)` turns the cache off, the shared read stays.
314
310
  *
315
311
  * @param {string} fullpath
316
312
  * @param {import("fs").Stats} stat
@@ -407,12 +403,11 @@ class Application extends Router {
407
403
  }
408
404
  value = value == null ? undefined : value.map((m) => m.toUpperCase());
409
405
  } else if (key === "etag") {
410
- // The skips are not taken back here. They used to be, because send consults freshness
411
- // and the skip branch looked like it had not copied the headers for it, but that
412
- // branch reads if-none-match, if-modified-since and cache-control by name whatever
413
- // this setting says, see request.js:527, and req.fresh reads nothing else off the
414
- // request. Registering a route or a middleware after listen still takes them back,
415
- // see router.js:1615: that is a different question, about code the analysis never saw.
406
+ // The skips are not taken back here. They used to be, because send consults freshness,
407
+ // but that branch reads if-none-match, if-modified-since and cache-control by name
408
+ // whatever this setting says, and req.fresh reads nothing else off the request.
409
+ // Registering a route after listen still takes them back: that is a different
410
+ // question, about code the analysis never saw
416
411
  if (typeof value === "function") {
417
412
  this._settings["etag fn"] = value;
418
413
  } else {
@@ -484,12 +479,14 @@ class Application extends Router {
484
479
  * is held in a set until it finishes, so close() knows when the last one is done. Native
485
480
  * routes and the catch-all both come through here, since both call it on the app.
486
481
  *
487
- * @param {any} res uWS response
488
- * @param {any} req uWS request, readable only during this call
489
- * @param {any} [preset] a literal registration's constants, see nativePreset in the router
490
- * @param {any} [skipHolder] where a granted header skip lives, forwarded whole: dropping
491
- * it here silently turned every skip off, since the native closures call this override
492
- * @returns {any} the request, with the response reachable as request.res
482
+ * @param {import("uWebSockets.js").HttpResponse} res uWS response
483
+ * @param {import("uWebSockets.js").HttpRequest} req uWS request, readable only during this call
484
+ * @param {import("./router-utils.js").NativePreset} [preset] a literal registration's constants,
485
+ * see nativePreset in the router
486
+ * @param {import("./router-utils.js").SkipHolder} [skipHolder] where a granted header skip lives,
487
+ * forwarded whole: dropping it here silently turned every skip off, since the native closures
488
+ * call this override
489
+ * @returns {Request} the request, with the response reachable as request.res
493
490
  */
494
491
  handleRequest(res, req, preset, skipHolder) {
495
492
  const request = super.handleRequest(res, req, preset, skipHolder);
@@ -523,8 +520,8 @@ class Application extends Router {
523
520
  * what the catch-all runs, and also what a native registration falls back to when it sees a
524
521
  * request it must not answer itself, see the case guard in Router#_registerUwsRoute.
525
522
  *
526
- * @param {any} res the uWS response
527
- * @param {any} req the uWS request
523
+ * @param {import("uWebSockets.js").HttpResponse} res the uWS response
524
+ * @param {import("uWebSockets.js").HttpRequest} req the uWS request
528
525
  */
529
526
  _serveGeneric(res, req) {
530
527
  const request = this.handleRequest(res, req);
@@ -551,9 +548,8 @@ class Application extends Router {
551
548
  *
552
549
  * Returns the app and not an `http.Server`, since there is no node server underneath. The app
553
550
  * carries `address()`, `close()`, `listening` and the 'listening' and 'close' events; anything
554
- * needing a real server, socket.io being the usual case, wants `app.uwsApp`. The callback runs
555
- * on the next tick with the bind error, if there was one. A path instead of a port is a unix
556
- * socket.
551
+ * needing a real server, socket.io being the usual case, wants `app.uwsApp`. A path instead of
552
+ * a port is a unix socket.
557
553
  *
558
554
  * @param {number|string} [port] port, or a unix socket path; 0 picks a free port
559
555
  * @param {string} [host] interface to bind; every interface when omitted
@@ -563,14 +559,12 @@ class Application extends Router {
563
559
  */
564
560
  listen(port, host, backlog, callback) {
565
561
  // With { cluster } the primary has nothing to bind. Each worker binds this same port with
566
- // µWS's shared flag, which is SO_REUSEPORT, so the kernel hands each connection to one of
567
- // them and the primary is not in the path at all: it forks, replaces a worker that dies,
568
- // and nothing else. Everything below this runs in the workers, listen callback included,
569
- // so it runs once per worker rather than once.
562
+ // uWS's shared flag, SO_REUSEPORT, so the kernel hands each connection to one of them and
563
+ // the primary only forks and replaces a worker that dies. Everything below this runs in the
564
+ // workers, listen callback included, so once per worker rather than once.
570
565
  //
571
- // The test is the process and not this app: a second app on a TLS port, one without a
572
- // cluster setting of its own, would otherwise take that port here, exclusively, and every
573
- // worker would fail on it.
566
+ // The test is the process and not this app: a second app on a TLS port, without a cluster
567
+ // setting of its own, would take that port here exclusively and every worker would fail
574
568
  if (cluster.isPrimary && isSupervising()) {
575
569
  if (this._clusterWorkers > 0 && !this._clusterHandle) {
576
570
  this._clusterHandle = forkWorkers(this._clusterWorkers);
@@ -758,19 +752,18 @@ class Application extends Router {
758
752
  * `res.render()` is the one that responds.
759
753
  *
760
754
  * `app.locals` and `options._locals` are merged into the options, in that order, so a
761
- * per-request local wins over an application-wide one. Caching follows the "view cache"
762
- * setting unless `options.cache` says otherwise.
763
- *
764
- * A function in the options position is taken as the callback.
755
+ * per-request local wins. Caching follows the "view cache" setting unless `options.cache` says
756
+ * otherwise. A function in the options position is taken as the callback.
765
757
  *
766
758
  * @param {string} name view name, resolved against the "views" setting
767
- * @param {Record<string, any>} [options] locals for the view
759
+ * @param {Record<string, any>|((err: Error|null, html?: string) => void)} [options] locals for
760
+ * the view, or the callback in its place
768
761
  * @param {(err: Error|null, html?: string) => void} [callback] receives the rendered view. It
769
762
  * is what render is for, so leaving it out throws, as it does in Express
770
763
  */
771
764
  render(name, options, callback) {
772
765
  if (typeof options === "function") {
773
- callback = /** @type {any} */ (options);
766
+ callback = /** @type {(err: Error|null, html?: string) => void} */ (options);
774
767
  options = new NullObject();
775
768
  }
776
769
  // render exists to hand the result somewhere, so there is always a callback by this point:
@@ -844,15 +837,13 @@ class Application extends Router {
844
837
  /**
845
838
  * Stops accepting connections, lets in-flight requests finish, then emits 'close'.
846
839
  *
847
- * Node's server.close(), which Express hands back from listen(), only closes the listen
848
- * socket and waits for what is being served; uWS's close() forcefully terminates every
849
- * connection, so calling it first aborted whatever a graceful shutdown was waiting for.
850
- * It still runs, but only once the last pending response is done, to drop the idle
851
- * keep-alive connections nothing else would close.
840
+ * Node's server.close() only closes the listen socket and waits for what is being served; uWS's
841
+ * close() terminates every connection, so calling it first aborted whatever a graceful shutdown
842
+ * was waiting for. It still runs, but only once the last pending response is done, to drop the
843
+ * idle keep-alive connections nothing else would close.
852
844
  *
853
- * The callback is the first 'close' listener, so it runs before any added afterwards. Closing
854
- * a server that was not listening still calls back, with an ERR_SERVER_NOT_RUNNING error, the
855
- * way node does.
845
+ * The callback is the first 'close' listener. Closing a server that was not listening still
846
+ * calls back with ERR_SERVER_NOT_RUNNING, the way node does.
856
847
  *
857
848
  * @param {(err?: Error) => void} [callback] called once closed
858
849
  * @returns {this} the app, for chaining
@@ -932,10 +923,9 @@ class Application extends Router {
932
923
  // carries. Middleware that takes a whole app and calls it, vhost being the one everybody meets, was
933
924
  // given something it could not call.
934
925
  //
935
- // This was tried once before and reverted the same day, because a callable app broke supertest:
936
- // `request(app)` reads `typeof app === "function"` and wraps whatever it finds in
937
- // http.createServer, and there was nothing underneath that could serve node's IncomingMessage, so
938
- // every call timed out. src/node-shim.js is what closes that hole, and it is why this is safe now.
926
+ // Tried once before and reverted the same day, because a callable app broke supertest: `request(app)`
927
+ // reads `typeof app === "function"` and wraps what it finds in http.createServer, and there was
928
+ // nothing underneath that could serve node's IncomingMessage. src/node-shim.js closes that hole.
939
929
  module.exports = function (options) {
940
930
  return new Application(options)._asCallable();
941
931
  };
package/src/cli.js CHANGED
@@ -18,26 +18,25 @@ limitations under the License.
18
18
  // npx fulmine migrate [dir]
19
19
  //
20
20
  // Rewrites the module specifier and nothing else. An Express 5 app is a Fulmine app already, so
21
- // there is no code to translate: what there is instead is a short list of things that behave
22
- // differently, printed at the end, because no rewrite can find those for you.
21
+ // there is no code to translate. The short list of things that behave differently is printed at
22
+ // the end, because no rewrite can find those for you.
23
23
  //
24
24
  // npx fulmine profile [entry]
25
25
  //
26
- // Prints what listen() worked out about each route and normally keeps to itself: which ones µWS
27
- // answers on its own, which ones fell back to the ordinary router and why, and which ones were
28
- // compiled all the way down to a response written at startup.
26
+ // Prints what listen() worked out about each route: which ones uWS answers on its own, which ones
27
+ // fell back to the ordinary router and why, and which ones were compiled into a response.
29
28
  //
30
29
  // npx fulmine verify [dir]
31
30
  //
32
31
  // Whether this machine and this project can run it at all: the node version, the C library, the
33
- // µWebSockets.js binary, the base image a Dockerfile names. See src/verify.js.
32
+ // uWebSockets.js binary, the base image a Dockerfile names. See src/verify.js.
34
33
  //
35
34
  // npx fulmine override [dir]
36
35
  // npx fulmine angular [dir]
37
36
  //
38
- // The two things a project needs that are a line in a JSON file rather than a specifier in a source
39
- // file: the package manager substitution, for a framework that requires express in its own code,
40
- // and angular.json's externalDependencies. See src/adopt.js.
37
+ // The two things a project needs that are a line in a JSON file rather than a specifier in a
38
+ // source file: the package manager substitution, for a framework that requires express in its own
39
+ // code, and angular.json's externalDependencies. See src/adopt.js.
41
40
 
42
41
  const fs = require("fs");
43
42
  const path = require("path");
@@ -47,6 +46,9 @@ const { collectRoutes } = require("./testing.js");
47
46
  const { verify } = require("./verify.js");
48
47
  const { override, angular } = require("./adopt.js");
49
48
 
49
+ /** @typedef {import("./application.js").Application} Application */
50
+ /** @typedef {import("./router-utils.js").RouteEntry} RouteEntry */
51
+
50
52
  const FROM = "express";
51
53
  const TO = "fulmine.js";
52
54
 
@@ -146,14 +148,12 @@ function collectFiles(dir) {
146
148
  * A reader for the .ts files of the project being migrated, or null when it has no TypeScript.
147
149
  *
148
150
  * acorn cannot read TypeScript, and shipping a parser that can would put megabytes into this
149
- * package for a command most people run once. A TypeScript project already has the compiler, so
150
- * it is resolved from there. A project without one is told its .ts files were left alone rather
151
- * than having them quietly skipped, which is what happened before they were looked at at all.
151
+ * package for a command most people run once. A TypeScript project already has the compiler, so it
152
+ * is resolved from there. A project without one is told its .ts files were left alone.
152
153
  *
153
- * typescript 7 is the compiler rewritten in Go, and it publishes no JavaScript parser any more:
154
- * require("typescript") gives back a version number and nothing else. Its scanner survives, on an
155
- * ESM-only subpath that require() reads on every node this package supports, so 7 gets the token
156
- * walk and 6 keeps the tree.
154
+ * typescript 7 is the compiler rewritten in Go and publishes no JavaScript parser any more:
155
+ * require("typescript") gives back a version number and nothing else. Its scanner survives on an
156
+ * ESM-only subpath that require() reads, so 7 gets the token walk and 6 keeps the tree.
157
157
  *
158
158
  * @param {string} target directory being migrated
159
159
  * @returns {((source: string, fileName: string, seen?: Set<string>) => {start: number, end: number}[])|null}
@@ -190,7 +190,7 @@ function loadTypeScript(target) {
190
190
  *
191
191
  * @param {string} source
192
192
  * @param {string} fileName decides whether JSX is allowed, so a .tsx angle bracket is not a cast
193
- * @param {any} ts the compiler
193
+ * @param {typeof import("typescript")} ts the compiler
194
194
  * @param {Set<string>} [seen] as in findSpecifiers
195
195
  * @returns {{start: number, end: number}[]}
196
196
  */
@@ -205,7 +205,7 @@ function findSpecifiersTypeScript(source, fileName, ts, seen) {
205
205
 
206
206
  /** @type {{start: number, end: number}[]} */
207
207
  const found = [];
208
- /** @param {any} node a string literal naming a module */
208
+ /** @param {import("typescript").StringLiteral} node a string literal naming a module */
209
209
  const take = (node) => {
210
210
  if (node.text === FROM) {
211
211
  found.push({ start: node.getStart(sourceFile), end: node.getEnd() });
@@ -252,7 +252,8 @@ function findSpecifiersTypeScript(source, fileName, ts, seen) {
252
252
  *
253
253
  * @param {string} source
254
254
  * @param {string} fileName decides whether JSX is allowed, as above
255
- * @param {any} ts the scanner and the two enums loadTypeScript kept
255
+ * @param {any} ts the scanner and the two enums loadTypeScript kept, from typescript 7's unstable
256
+ * API, which the typings this project compiles against do not describe
256
257
  * @param {Set<string>} [seen] as in findSpecifiers
257
258
  * @returns {{start: number, end: number}[]}
258
259
  */
@@ -303,7 +304,7 @@ function findSpecifiersScanner(source, fileName, ts, seen) {
303
304
  * @returns {{start: number, end: number}[]|null} null when the file does not parse
304
305
  */
305
306
  function findSpecifiers(source, seen) {
306
- /** @type {any} */
307
+ /** @type {import("acorn").Program|null|undefined} */
307
308
  let tree;
308
309
  // A file is either a module or a script and the parser has to be told which. Try module first,
309
310
  // since it also accepts everything a script can contain except a bare `return`.
@@ -311,7 +312,7 @@ function findSpecifiers(source, seen) {
311
312
  try {
312
313
  tree = acorn.parse(source, {
313
314
  ecmaVersion: "latest",
314
- sourceType: /** @type {any} */ (sourceType),
315
+ sourceType: /** @type {"module"|"script"} */ (sourceType),
315
316
  allowReturnOutsideFunction: true,
316
317
  allowAwaitOutsideFunction: true,
317
318
  allowHashBang: true
@@ -327,7 +328,7 @@ function findSpecifiers(source, seen) {
327
328
 
328
329
  /** @type {{start: number, end: number}[]} */
329
330
  const found = [];
330
- /** @param {any} node a string literal naming a module */
331
+ /** @param {import("acorn").Literal & {value: string}} node a string literal naming a module */
331
332
  const record = (node) => {
332
333
  if (node.value === FROM) {
333
334
  found.push({ start: node.start, end: node.end });
@@ -364,8 +365,10 @@ function findSpecifiers(source, seen) {
364
365
  * Visits every node. acorn produces plain objects, so the shape is walked rather than dispatched
365
366
  * on: a table of node types would have to be kept in step with the parser, and being out of step
366
367
  * would mean silently skipping an import.
367
- * @param {any} node
368
- * @param {(node: any) => void} visit
368
+ * @param {any} node an acorn node, or an array or a scalar under one: walked by key, so no shape
369
+ * is assumed
370
+ * @param {(node: any) => void} visit handed every node, loose because the visitor reads edges of
371
+ * its own off each
369
372
  */
370
373
  function walk(node, visit) {
371
374
  if (!node || typeof node !== "object") return;
@@ -453,17 +456,16 @@ function findEntry(given) {
453
456
  /**
454
457
  * Every build of this library the application could load, as the prototype that owns listen().
455
458
  *
456
- * The command runs from its own copy, and the application loads whichever one resolves from its
457
- * own directory. That is usually the same file and sometimes is not: a global install, an
458
- * `npx fulmine.js@version`, a workspace that hoisted a second copy, or the `express` name pointing
459
- * here through an override. Patching only this command's copy leaves the application's own listen()
460
- * to bind the port, and the command then reports that the file built nothing.
459
+ * The command runs from its own copy, and the application loads whichever one resolves from its own
460
+ * directory. That is usually the same file and sometimes not: a global install, an
461
+ * `npx fulmine.js@version`, a hoisted second copy, or `express` pointing here through an override.
462
+ * Patching only this command's copy leaves the application's own listen() to bind the port.
461
463
  *
462
- * An app is a callable, so its own prototype is not the one that carries the methods: walk up to
463
- * whichever link owns listen.
464
+ * An app is a callable, so its own prototype does not carry the methods: walk up to whichever link
465
+ * owns listen.
464
466
  *
465
467
  * @param {string} entry
466
- * @returns {any[]} the prototypes to stub, this command's copy first
468
+ * @returns {object[]} the prototypes to stub, this command's copy first
467
469
  */
468
470
  function listenOwners(entry) {
469
471
  const builds = new Set([require("./index.js")]);
@@ -511,7 +513,7 @@ function listenOwners(entry) {
511
513
  *
512
514
  * @param {string[]} argv
513
515
  * @param {string} command the word for the message when there is nothing to load
514
- * @returns {{apps: any[], entry: string}|null} null once the reason has been printed
516
+ * @returns {{apps: Application[], entry: string}|null} null once the reason has been printed
515
517
  */
516
518
  function loadApps(argv, command) {
517
519
  const entry = findEntry(argv.find((arg) => !arg.startsWith("--")));
@@ -543,7 +545,7 @@ function loadApps(argv, command) {
543
545
  try {
544
546
  require(entry);
545
547
  } catch (e) {
546
- const error = /** @type {any} */ (e);
548
+ const error = /** @type {Error} */ (e);
547
549
  restore();
548
550
  console.error(`${path.relative(process.cwd(), entry)} could not be loaded:
549
551
  ${error.stack ?? error}`);
@@ -580,16 +582,15 @@ ${error.stack ?? error}`);
580
582
  /**
581
583
  * Ends the file-reading threads that building an application started.
582
584
  *
583
- * An Application starts one per `threads` in its constructor, and these commands only ever read
584
- * what compiling the routes decided: nothing here serves a file, so nothing here needs a thread.
585
- * They are unref'd, so leaving them would not hang the process, but they are threads holding the
586
- * library the application loaded, and this command is often not the whole process. It also stops
587
- * them outliving the directory they were loaded from, which is how a test that profiles a copy and
588
- * then removes it saw "Cannot find module .../src/worker.js" arrive after it had finished.
585
+ * An Application starts one per `threads` in its constructor, and these commands only read what
586
+ * compiling the routes decided. They are unref'd, so leaving them would not hang the process, but
587
+ * they hold the library the application loaded, and this command is often not the whole process.
588
+ * It also stops them outliving the directory they were loaded from, which is how a test that
589
+ * profiles a copy and then removes it saw "Cannot find module .../src/worker.js".
589
590
  *
590
- * Best effort throughout: a build with no workers, or a worker already gone, is not an error here.
591
+ * Best effort throughout: a build with no workers, or a worker already gone, is not an error.
591
592
  *
592
- * @param {any[]} apps
593
+ * @param {Application[]} apps
593
594
  * @returns {void}
594
595
  */
595
596
  function stopFileWorkers(apps) {
@@ -765,12 +766,11 @@ function matchesWanted(full, method, wanted) {
765
766
  *
766
767
  * There is no score here on purpose. A percentage of routes is not a percentage of traffic: an
767
768
  * application with a thousand cold routes and one hot one that fell back would score well and
768
- * serve badly. What is printed instead is counted rather than judged, and the advice is only
769
- * printed for the reasons somebody can actually act on.
769
+ * serve badly. What is printed is counted rather than judged.
770
770
  *
771
- * @param {any[]} routes
772
- * @param {any[]} native
773
- * @param {any[]} declarative
771
+ * @param {{route: RouteEntry, full: string}[]} routes
772
+ * @param {{route: RouteEntry, full: string}[]} native
773
+ * @param {{route: RouteEntry, full: string}[]} declarative
774
774
  */
775
775
  function printSummary(routes, native, declarative) {
776
776
  console.log("\nWhat this adds up to\n");
@@ -815,7 +815,7 @@ function printSummary(routes, native, declarative) {
815
815
  }
816
816
 
817
817
  /**
818
- * @param {any} app
818
+ * @param {Application} app the application the entry file built
819
819
  * @param {boolean} several whether to say which application this is
820
820
  */
821
821
  function printProfile(app, several) {