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 +2 -1
- package/src/adopt.js +20 -26
- package/src/application.js +63 -73
- package/src/cli.js +48 -48
- package/src/cluster.js +18 -27
- package/src/compression.js +141 -112
- package/src/declarative.js +611 -540
- package/src/hot-settings.js +80 -0
- package/src/index.js +11 -17
- package/src/lazy-readable.js +131 -0
- package/src/lazy-writable.js +97 -0
- package/src/middlewares.js +153 -130
- package/src/nest.js +22 -36
- package/src/node-shim.js +19 -16
- package/src/optimizer.js +600 -0
- package/src/options.d.ts +9 -4
- package/src/parse-query.js +3 -3
- package/src/request-utils.js +307 -0
- package/src/request.js +147 -548
- package/src/response-utils.js +88 -0
- package/src/response.js +228 -535
- package/src/route.js +7 -8
- package/src/router-utils.js +998 -0
- package/src/router.js +166 -2170
- package/src/server-shape.js +40 -51
- package/src/server-timing.js +32 -33
- package/src/socket.js +208 -0
- package/src/testing.js +43 -45
- package/src/usage.js +25 -25
- package/src/utils.js +165 -78
- package/src/verify.js +22 -31
- package/src/view.js +6 -8
- package/src/walk.js +581 -0
- package/src/websocket.js +34 -26
- package/src/work.js +22 -28
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fulmine.js",
|
|
3
|
-
"version": "5.19.
|
|
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
|
|
21
|
-
//
|
|
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
|
|
24
|
-
//
|
|
25
|
-
//
|
|
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
|
|
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
|
|
76
|
-
*
|
|
77
|
-
*
|
|
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
|
|
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
|
|
239
|
-
*
|
|
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
|
|
261
|
-
*
|
|
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,
|
|
271
|
-
//
|
|
272
|
-
//
|
|
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")) {
|
package/src/application.js
CHANGED
|
@@ -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
|
-
*
|
|
158
|
-
*
|
|
159
|
-
* @param {
|
|
160
|
-
* @param {
|
|
161
|
-
* @param {
|
|
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
|
-
*
|
|
170
|
-
*
|
|
171
|
-
* @param {
|
|
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:
|
|
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 {
|
|
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:
|
|
278
|
-
* @param {(err:
|
|
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
|
-
*
|
|
311
|
-
*
|
|
312
|
-
*
|
|
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
|
-
//
|
|
412
|
-
//
|
|
413
|
-
//
|
|
414
|
-
//
|
|
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 {
|
|
488
|
-
* @param {
|
|
489
|
-
* @param {
|
|
490
|
-
*
|
|
491
|
-
*
|
|
492
|
-
*
|
|
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 {
|
|
527
|
-
* @param {
|
|
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`.
|
|
555
|
-
*
|
|
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
|
-
//
|
|
567
|
-
//
|
|
568
|
-
//
|
|
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,
|
|
572
|
-
//
|
|
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
|
|
762
|
-
*
|
|
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
|
|
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 {
|
|
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()
|
|
848
|
-
*
|
|
849
|
-
*
|
|
850
|
-
*
|
|
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
|
|
854
|
-
*
|
|
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
|
-
//
|
|
936
|
-
//
|
|
937
|
-
//
|
|
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
|
|
22
|
-
//
|
|
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
|
|
27
|
-
//
|
|
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
|
-
//
|
|
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
|
|
39
|
-
// file: the package manager substitution, for a framework that requires express in its own
|
|
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
|
-
*
|
|
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
|
|
154
|
-
* require("typescript") gives back a version number and nothing else. Its scanner survives
|
|
155
|
-
* ESM-only subpath that require() reads
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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
|
-
*
|
|
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
|
-
*
|
|
458
|
-
* `npx fulmine.js@version`, a
|
|
459
|
-
*
|
|
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
|
|
463
|
-
*
|
|
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 {
|
|
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:
|
|
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 {
|
|
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
|
|
584
|
-
*
|
|
585
|
-
*
|
|
586
|
-
*
|
|
587
|
-
*
|
|
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
|
|
591
|
+
* Best effort throughout: a build with no workers, or a worker already gone, is not an error.
|
|
591
592
|
*
|
|
592
|
-
* @param {
|
|
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
|
|
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 {
|
|
772
|
-
* @param {
|
|
773
|
-
* @param {
|
|
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 {
|
|
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) {
|