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/src/utils.js CHANGED
@@ -28,6 +28,21 @@ const ms = require("ms");
28
28
  const fs = require("fs");
29
29
  const { Stats } = require("fs");
30
30
 
31
+ /** @typedef {import("./request.js")} Request */
32
+ /** @typedef {import("./response.js")} Response */
33
+ /**
34
+ * An error with the http-errors fields on it, which are what `res.status(err.status || 500)`, the
35
+ * error page and the body parsers read. None is required: a plain throw carries none of them.
36
+ * @typedef {Error & {
37
+ * status?: number,
38
+ * statusCode?: number,
39
+ * expose?: boolean,
40
+ * code?: string,
41
+ * type?: string,
42
+ * types?: string[]
43
+ * }} HttpError
44
+ */
45
+
31
46
  const EMPTY_REGEX = new RegExp(``);
32
47
 
33
48
  // what express hands qs for a query string. allowPrototypes keeps a key named "constructor" or
@@ -231,11 +246,9 @@ function patternToRegex(pattern, isPrefix = false, caseSensitive = true, strict
231
246
  let lastWildcardEnd = -1;
232
247
  // What path-to-regexp calls the wildcard backtrack: the literal text written since the last
233
248
  // wildcard. Once a wildcard has eaten slashes, a later one in the same path is held to a single
234
- // segment, or the two would divide the path between them in more than one way and the regex
235
- // would have to backtrack to find out which. /*a/*b against /x/y/ is the case that shows it:
236
- // express refuses it under strict routing, and a second greedy wildcard accepts it.
237
- // the text written since the last capture of any kind, and the text written since the last
238
- // wildcard, which are the two path-to-regexp weighs
249
+ // segment, or the two would divide the path in more than one way and the regex would have to
250
+ // backtrack. /*a/*b against /x/y/ shows it: express refuses it under strict routing.
251
+ // Two counters: text since the last capture of any kind, and text since the last wildcard
239
252
  let backtrack = "";
240
253
  let wildcardBacktrack = "";
241
254
  let lastCaptureWasWildcard = false;
@@ -405,16 +418,13 @@ function patternToRegex(pattern, isPrefix = false, caseSensitive = true, strict
405
418
  i++;
406
419
 
407
420
  // When a :parameter precedes this group, that parameter is the one that gives ground
408
- // while backtracking, so this one must not swallow the separator as well. Express
409
- // splits /a.b.c against /:file{.:ext} as file=a.b, ext=c, which only works if ext
410
- // cannot contain a dot. After static text there is nothing to give ground, so the
411
- // parameter takes everything: /file{.:ext} against /file.tar.gz gives ext=tar.gz.
412
- // The whole of it, not its first character: /:foo{abc:bar} against /123abcabc splits as
413
- // foo=123 and bar=abc on express, and reading the separator as "a" left bar unable to
414
- // match its own text, so the group never matched and foo took the segment whole. More
415
- // than one character cannot go in a class, so it is written as a lookahead, and either
416
- // way the parameter is still allowed to be exactly the separator, as path-to-regexp
417
- // writes it.
421
+ // while backtracking, so this one must not swallow the separator too. Express splits
422
+ // /a.b.c against /:file{.:ext} as file=a.b, ext=c, which only works if ext cannot
423
+ // contain a dot. After static text nothing gives ground, so the parameter takes
424
+ // everything: /file{.:ext} against /file.tar.gz gives ext=tar.gz.
425
+ // The whole separator, not its first character: /:foo{abc:bar} against /123abcabc
426
+ // splits as foo=123 and bar=abc on express, and reading it as "a" left bar unable to
427
+ // match its own text. More than one character cannot go in a class, so it is a lookahead
418
428
  const colon = groupContent.indexOf(":");
419
429
  const separator = lastTokenWasParam && colon > 0 ? groupContent.slice(0, colon) : "";
420
430
  const escapedSeparator = separator.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -445,13 +455,11 @@ function patternToRegex(pattern, isPrefix = false, caseSensitive = true, strict
445
455
  }
446
456
  }
447
457
  if (lastWildcard && lastWildcardEnd === regexPattern.length) {
448
- // A wildcard immediately before the group. `(?<w>[^]+)(?:group)?` can never let
449
- // the group match, because the wildcard is greedy and the group may be empty, and
450
- // making the wildcard lazy is not the same thing either: it gives the trailing
451
- // slash away, and /*path{.:ext} against /a/b/ then loses the empty last segment.
452
- // path-to-regexp writes the two branches out instead, group first and the
453
- // wildcard greedy in both, so that is what goes here. The second branch captures
454
- // the same parameter under a name of its own, which is what uniqueGroupName is for.
458
+ // A wildcard immediately before the group. `(?<w>[^]+)(?:group)?` can never let the
459
+ // group match, because the wildcard is greedy and the group may be empty, and a
460
+ // lazy wildcard is not the same thing either: it gives the trailing slash away, and
461
+ // /*path{.:ext} against /a/b/ loses the empty last segment. path-to-regexp writes
462
+ // the two branches out instead, group first and the wildcard greedy in both
455
463
  const second = uniqueGroupName(lastWildcard.name);
456
464
  wildcardNames.push(second);
457
465
  const withWildcard = regexPattern.slice(lastWildcard.start);
@@ -619,13 +627,13 @@ const NOT_A_LITERAL = /[:*{}\\]/;
619
627
  *
620
628
  * The answer is structural: no position where two different literals meet, and, when neither path
621
629
  * can change length, the same number of segments. `/orders/:id` and `/invoices/:id` cannot both
622
- * match, `/users/:id` and `/users/me` can. The caller reads "do not know" as yes, so every doubt
623
- * answers true: saying two paths overlap only costs a native registration, while missing one lets
624
- * µWS answer a request that belonged to an earlier route.
630
+ * match, `/users/:id` and `/users/me` can. The caller reads "do not know" as yes: saying two paths
631
+ * overlap only costs a native registration, while missing one lets uWS answer a request that
632
+ * belonged to an earlier route.
625
633
  *
626
634
  * A parameter is not the only shape that matches more than itself. A wildcard and an optional group
627
- * do too, and reading `{:opt}` or `*splat` as the literal text it is written as reported "cannot
628
- * overlap" for a route that plainly could, which took the earlier route's turn away.
635
+ * do too, and reading `{:opt}` or `*splat` as literal text reported "cannot overlap" for a route
636
+ * that plainly could.
629
637
  *
630
638
  * @param {string} a
631
639
  * @param {string} b
@@ -759,11 +767,8 @@ function acceptParams(str) {
759
767
  //
760
768
  // The keys are media types, so an application uses a handful and the ceiling is never approached.
761
769
  // It is here because application code is free to hand res.type() something a client sent, and an
762
- // unbounded map keyed on that is a leak the client controls.
763
- //
764
- // Clearing beats evicting one entry at a time: the few types an application really uses are back
765
- // within a few requests, whereas refusing new entries once full would let a flood of invented
766
- // values lock the real ones out for the life of the process.
770
+ // unbounded map keyed on that is a leak the client controls. Clearing beats evicting one at a
771
+ // time: refusing new entries once full would let a flood of invented values lock the real ones out.
767
772
  const MEMO_LIMIT = 512;
768
773
 
769
774
  /**
@@ -772,8 +777,9 @@ const MEMO_LIMIT = 512;
772
777
  * The wrapped function must never answer undefined, since that is what the cache reads as a miss.
773
778
  * The mime lookups here answer false for something they do not know, which caches correctly.
774
779
  *
775
- * @param {(key: string) => any} fn
776
- * @returns {(key: string) => any}
780
+ * @template T
781
+ * @param {(key: string) => T} fn
782
+ * @returns {(key: string) => T}
777
783
  */
778
784
  function memoizeByString(fn) {
779
785
  const cache = new Map();
@@ -818,7 +824,7 @@ function normalizeType(type) {
818
824
  * unicode escapes, so a string in the body cannot close a script tag in an HTML page that embeds
819
825
  * the response.
820
826
  *
821
- * @param {any} value
827
+ * @param {unknown} value whatever the handler passed to res.json
822
828
  * @param {any} [replacer] the "json replacer" setting
823
829
  * @param {string|number} [spaces] the "json spaces" setting
824
830
  * @param {boolean} [escape] the "json escape" setting
@@ -855,14 +861,12 @@ const ENCODING_ANY = ENCODING_BR | ENCODING_GZIP | ENCODING_DEFLATE | ENCODING_Z
855
861
  /**
856
862
  * The encoding to answer with, read straight off Accept-Encoding rather than through negotiator:
857
863
  * the header is a short list of names with an optional q, and building a Negotiator per response
858
- * to read it costs more than the scan does.
864
+ * costs more than the scan does. The tie-break is negotiator's, for the list the compression module
865
+ * hands it: brotli first, then gzip, then deflate, and identity last.
859
866
  *
860
- * The tie-break is negotiator's, for the list the compression module hands it: brotli first, then
861
- * gzip, then deflate, and identity last.
862
- *
863
- * Only the encodings named in `allowed` are on offer, since the caller may not be able to
864
- * produce all three: express.static offers the two it can have lying on disk. An uncompressed
865
- * answer is always on offer, and is what an empty header ends up choosing.
867
+ * Only the encodings named in `allowed` are on offer, since the caller may not produce all three:
868
+ * express.static offers the two it can have lying on disk. An uncompressed answer is always on
869
+ * offer, and is what an empty header chooses.
866
870
  *
867
871
  * @param {string} accept the header, or "" when the request carried none
868
872
  * @param {number} allowed ENCODING_BR, ENCODING_ZSTD, ENCODING_GZIP and ENCODING_DEFLATE, or'd
@@ -989,12 +993,10 @@ const defaultSettings = {
989
993
  // The native µWS router matches bytes, so the compiler in _compileOptimizedRoutes only hands
990
994
  // it routes whose earlier siblings it can prove agree under either case rule.
991
995
  "declarative responses": true,
992
- // on. Off hands every request to the ordinary chain instead of letting µWS match what it can,
996
+ // on. Off hands every request to the ordinary chain instead of letting uWS match what it can,
993
997
  // which is slower and answers the same. Not a tuning knob: it exists so one application can be
994
- // served both ways and the two sets of answers compared, which tests the optimizer against the
995
- // rest of the framework without a second framework to compare with. See
996
- // `npm run fuzz -- --self`. A compiled response needs a native registration to hang on, so this
997
- // takes "declarative responses" with it.
998
+ // served both ways and the answers compared, see `npm run fuzz -- --self`. A compiled response
999
+ // needs a native registration to hang on, so this takes "declarative responses" with it
998
1000
  "native routes": true,
999
1001
  // off: with a window set, the size and mtime of a file served by sendFile are remembered for
1000
1002
  // it, which is one syscall less per request and a file that can be served as it was a moment
@@ -1053,13 +1055,17 @@ function cachedStat(file, ttl) {
1053
1055
  /**
1054
1056
  * A duration setting as milliseconds: false is off, a string is read by ms, a number is itself.
1055
1057
  *
1056
- * @param {any} value
1058
+ * @param {string|number|boolean|undefined} value the setting as the application wrote it
1057
1059
  * @param {string} name for the error, which names the setting the application wrote
1058
1060
  * @returns {number}
1059
1061
  */
1060
1062
  function durationSetting(value, name) {
1061
1063
  const parsed =
1062
- value === false || value === undefined ? 0 : typeof value === "string" ? ms(/** @type {any} */ (value)) : value;
1064
+ value === false || value === undefined
1065
+ ? 0
1066
+ : typeof value === "string"
1067
+ ? ms(/** @type {import("ms").StringValue} */ (value))
1068
+ : value;
1063
1069
  if (typeof parsed !== "number" || !(parsed >= 0)) {
1064
1070
  throw new TypeError(`${name} must be a duration`);
1065
1071
  }
@@ -1140,8 +1146,9 @@ function deprecated(oldMethod, newMethod, full = false) {
1140
1146
  * request, each time picking up after the route it just ran, and Array.findIndex has no way to
1141
1147
  * start anywhere but the beginning.
1142
1148
  *
1143
- * @param {any[]} arr
1144
- * @param {(item: any, index: number, arr: any[]) => boolean} fn
1149
+ * @template T
1150
+ * @param {T[]} arr
1151
+ * @param {(item: T, index: number, arr: T[]) => boolean} fn
1145
1152
  * @param {number} [index] where to start
1146
1153
  * @returns {number} the index, or -1
1147
1154
  */
@@ -1176,7 +1183,7 @@ function decode(path) {
1176
1183
  *
1177
1184
  * @param {string} value
1178
1185
  * @returns {string}
1179
- * @throws {any} carrying status 400 when the value cannot be decoded
1186
+ * @throws {HttpError} carrying status 400 when the value cannot be decoded
1180
1187
  */
1181
1188
  function decodeParam(value) {
1182
1189
  // the common case, and worth the check: a parameter is usually a number or a word, and
@@ -1189,7 +1196,8 @@ function decodeParam(value) {
1189
1196
  } catch {
1190
1197
  // a URIError, not an Error: express throws what decodeURIComponent threw, so an error
1191
1198
  // handler written as `err instanceof URIError` has to keep working here
1192
- const err = /** @type {any} */ (new URIError(`Failed to decode param '${value}'`));
1199
+ /** @type {HttpError} */
1200
+ const err = new URIError(`Failed to decode param '${value}'`);
1193
1201
  err.status = 400;
1194
1202
  err.statusCode = 400;
1195
1203
  err.expose = true;
@@ -1274,8 +1282,8 @@ function parseHttpDate(date) {
1274
1282
  * no longer the current one, which is a 412 rather than a 304: the client asked to be stopped if
1275
1283
  * anything had changed.
1276
1284
  *
1277
- * @param {any} req
1278
- * @param {any} res
1285
+ * @param {Request} req
1286
+ * @param {Response} res
1279
1287
  * @returns {boolean}
1280
1288
  */
1281
1289
  function isPreconditionFailure(req, res) {
@@ -1296,7 +1304,8 @@ function isPreconditionFailure(req, res) {
1296
1304
  // if-unmodified-since
1297
1305
  const unmodifiedSince = parseHttpDate(req.headers["if-unmodified-since"]);
1298
1306
  if (!isNaN(unmodifiedSince)) {
1299
- const lastModified = parseHttpDate(res.get("Last-Modified"));
1307
+ // cast because res.get answers an array for set-cookie, and never for this one
1308
+ const lastModified = parseHttpDate(/** @type {string|undefined} */ (res.get("Last-Modified")));
1300
1309
  return isNaN(lastModified) || lastModified > unmodifiedSince;
1301
1310
  }
1302
1311
 
@@ -1345,7 +1354,7 @@ function statTag(stat, weak) {
1345
1354
  * ETag comes from its size and mtime while a body's comes from its contents.
1346
1355
  *
1347
1356
  * @param {{weak: boolean}} options
1348
- * @returns {(body: any, encoding?: BufferEncoding) => string}
1357
+ * @returns {(body: string|Buffer|import("fs").Stats, encoding?: BufferEncoding) => string}
1349
1358
  */
1350
1359
  function createETagGenerator(options) {
1351
1360
  return function generateETag(body, encoding) {
@@ -1367,24 +1376,26 @@ function createETagGenerator(options) {
1367
1376
  * and sending the whole file. It may carry either an ETag or a date, and a date only counts when
1368
1377
  * it matches Last-Modified exactly.
1369
1378
  *
1370
- * @param {any} req
1371
- * @param {any} res
1379
+ * @param {Request} req
1380
+ * @param {Response} res
1372
1381
  * @returns {boolean}
1373
1382
  */
1374
1383
  function isRangeFresh(req, res) {
1375
- const ifRange = req.headers["if-range"];
1384
+ // folded to one string, as every header but set-cookie is
1385
+ const ifRange = /** @type {string|undefined} */ (req.headers["if-range"]);
1376
1386
  if (!ifRange) {
1377
1387
  return true;
1378
1388
  }
1379
1389
 
1380
1390
  // if-range as etag
1381
1391
  if (ifRange.indexOf('"') !== -1) {
1382
- const etag = res.get("etag");
1392
+ const etag = /** @type {string|undefined} */ (res.get("etag"));
1383
1393
  return Boolean(etag && ifRange.indexOf(etag) !== -1);
1384
1394
  }
1385
1395
 
1386
1396
  // if-range as modified date
1387
- const lastModified = res.get("Last-Modified");
1397
+ // cast because res.get answers an array for set-cookie, and never for this one
1398
+ const lastModified = /** @type {string|undefined} */ (res.get("Last-Modified"));
1388
1399
  return parseHttpDate(lastModified) <= parseHttpDate(ifRange);
1389
1400
  }
1390
1401
 
@@ -1489,10 +1500,9 @@ const HEADER_VALUE = /[^\t\x20-\x7e\x80-\xff]/;
1489
1500
  * One of node's header errors, built the way node builds it.
1490
1501
  *
1491
1502
  * Assigning the code is not the whole of it. Node also puts the code in the first line of the
1492
- * stack, by naming the error "TypeError [THE_CODE]" while V8 formats that line and then taking
1493
- * the name back off. Whatever prints a stack therefore says which code it was, and the default
1494
- * error page prints exactly that: without this, the same refusal reads "TypeError:" here and
1495
- * "TypeError [ERR_INVALID_CHAR]:" behind Express. Found by fuzzing against express.
1503
+ * stack, by naming the error "TypeError [THE_CODE]" while V8 formats that line and then taking the
1504
+ * name back off. The default error page prints exactly that: without this, the same refusal reads
1505
+ * "TypeError:" here and "TypeError [ERR_INVALID_CHAR]:" behind Express. Found by fuzzing.
1496
1506
  *
1497
1507
  * @param {string} message
1498
1508
  * @param {string} code
@@ -1506,16 +1516,73 @@ function headerError(message, code) {
1506
1516
  void err.stack;
1507
1517
  // back to the prototype's "TypeError", which is what node leaves behind. Cast because Error
1508
1518
  // declares name as always present, and this deletes the own property to uncover it again
1509
- delete (/** @type {any} */ (err).name);
1519
+ delete (/** @type {{name?: string}} */ (err).name);
1510
1520
  err.code = code;
1511
1521
  return err;
1512
1522
  }
1513
1523
 
1524
+ /**
1525
+ * node's ERR_HTTP_HEADERS_SENT, for a head that can no longer change: "set" from setHeader,
1526
+ * "remove" from removeHeader, "write" from writeHead, which is how node words each one.
1527
+ *
1528
+ * @param {string} verb
1529
+ * @returns {NodeJS.ErrnoException}
1530
+ */
1531
+ function headersSentError(verb) {
1532
+ /** @type {NodeJS.ErrnoException} */
1533
+ const err = new Error(`Cannot ${verb} headers after they are sent to the client`);
1534
+ // the first line of the stack reads as node's, see headerError
1535
+ err.name = "Error [ERR_HTTP_HEADERS_SENT]";
1536
+ void err.stack;
1537
+ delete (/** @type {{name?: string}} */ (err).name);
1538
+ err.code = "ERR_HTTP_HEADERS_SENT";
1539
+ return err;
1540
+ }
1541
+
1542
+ /**
1543
+ * Applies the headers a writeHead call carries, in either of node's shapes, (status, headers) or
1544
+ * (status, reason, headers), and answers the reason phrase when there was one. Shared with the
1545
+ * middleware that hooks writeHead: what it decides at the head has to see these first, the way
1546
+ * on-headers applies them before its listeners run.
1547
+ *
1548
+ * @param {{setHeader(name: string, value: import("http").OutgoingHttpHeader|undefined): unknown}} res
1549
+ * @param {string|import("http").OutgoingHttpHeaders|import("http").OutgoingHttpHeader[]} [statusMessage]
1550
+ * @param {import("http").OutgoingHttpHeaders|import("http").OutgoingHttpHeader[]} [headers]
1551
+ * @returns {string|undefined} the reason phrase
1552
+ */
1553
+ function applyWriteHead(res, statusMessage, headers) {
1554
+ let reason;
1555
+ if (typeof statusMessage === "string") {
1556
+ reason = statusMessage;
1557
+ } else if (!headers) {
1558
+ // the two-argument shape, where what looked like a reason phrase is the headers
1559
+ headers = statusMessage;
1560
+ }
1561
+ if (Array.isArray(headers)) {
1562
+ // node takes a flat list here, name then value, and not a list of pairs. An odd length is
1563
+ // the caller's mistake and node names the argument in what it throws
1564
+ if (headers.length % 2 !== 0) {
1565
+ /** @type {NodeJS.ErrnoException} */
1566
+ const err = new TypeError(`The argument 'headers' is invalid. Received ${JSON.stringify(headers)}`);
1567
+ err.code = "ERR_INVALID_ARG_VALUE";
1568
+ throw err;
1569
+ }
1570
+ for (let i = 0; i < headers.length; i += 2) {
1571
+ res.setHeader(/** @type {string} */ (headers[i]), headers[i + 1]);
1572
+ }
1573
+ } else if (headers) {
1574
+ for (const header in headers) {
1575
+ res.setHeader(header, headers[header]);
1576
+ }
1577
+ }
1578
+ return reason;
1579
+ }
1580
+
1514
1581
  /**
1515
1582
  * Refuses a header name that is not an HTTP token, the way node's setHeader does and with its
1516
1583
  * error, so an application catching ERR_INVALID_HTTP_TOKEN behind Express catches it here.
1517
1584
  *
1518
- * @param {any} name
1585
+ * @param {unknown} name whatever a caller passed as a header name, which is what is being checked
1519
1586
  * @returns {void}
1520
1587
  * @throws {TypeError} if the name is not a token, which includes not being a string
1521
1588
  */
@@ -1567,7 +1634,7 @@ function validateHeaderValue(name, value) {
1567
1634
  * again is what turns one bad header into a dead process.
1568
1635
  *
1569
1636
  * @param {string} name
1570
- * @param {any} value
1637
+ * @param {any} value whatever a caller passed as a header value
1571
1638
  * @returns {boolean}
1572
1639
  */
1573
1640
  function headerIsWritable(name, value) {
@@ -1589,21 +1656,39 @@ const STAT_ERROR_STATUS = { ENAMETOOLONG: 404, ENOTDIR: 404, ENOENT: 404 };
1589
1656
  * of that handler as 500. The message is the status's own name, as http-errors writes it.
1590
1657
  *
1591
1658
  * @param {number} status
1592
- * @returns {any}
1659
+ * @param {string} [message] the status text unless given, as http-errors has it
1660
+ * @returns {HttpError}
1593
1661
  */
1594
- function httpError(status) {
1595
- const message = statuses.message[status] ?? "Error";
1596
- const err = /** @type {any} */ (new Error(message));
1662
+ function httpError(status, message = statuses.message[status] ?? "Error") {
1663
+ /** @type {HttpError} */
1664
+ const err = new Error(message);
1597
1665
  // http-errors names these BadRequestError, ForbiddenError and so on, and the name is what the
1598
1666
  // error page shows: an application looking at a 400 sees the same word Express shows it. Set
1599
1667
  // before anything reads the stack, which V8 formats on first read
1600
- err.name = `${message.replace(/\W/g, "")}Error`;
1668
+ err.name = httpErrorName(status);
1601
1669
  err.expose = status < 500;
1602
1670
  err.statusCode = status;
1603
1671
  err.status = status;
1604
1672
  return err;
1605
1673
  }
1606
1674
 
1675
+ /**
1676
+ * The name http-errors gives an error for a status, NotFoundError for 404: each word of the status
1677
+ * text capitalised and run together, plus Error unless it already ends in it, which is how
1678
+ * "Internal Server Error" stays InternalServerError.
1679
+ *
1680
+ * @param {number} status
1681
+ * @returns {string}
1682
+ */
1683
+ function httpErrorName(status) {
1684
+ const name = (statuses.message[status] ?? "Error")
1685
+ .split(" ")
1686
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
1687
+ .join("")
1688
+ .replace(/[^ _0-9a-z]/gi, "");
1689
+ return name.endsWith("Error") ? name : name + "Error";
1690
+ }
1691
+
1607
1692
  /**
1608
1693
  * Marks an fs error the way send does before it is handed on, so an error handler reading
1609
1694
  * err.status or err.statusCode finds what it would find behind Express. The three properties are
@@ -1612,8 +1697,8 @@ function httpError(status) {
1612
1697
  *
1613
1698
  * The error itself is returned rather than a new one, so its errno, code, syscall and path survive.
1614
1699
  *
1615
- * @param {any} err
1616
- * @returns {any} the same error
1700
+ * @param {HttpError} err the fs error, which carries its errno and path
1701
+ * @returns {HttpError} the same error
1617
1702
  */
1618
1703
  function asStatError(err) {
1619
1704
  err.expose = false;
@@ -1626,8 +1711,7 @@ function asStatError(err) {
1626
1711
  // A constructor whose instances have no prototype, so a key from a request body or a query string
1627
1712
  // cannot reach Object.prototype. Typed as returning a plain record: without that, assigning one
1628
1713
  // reads as assigning `any`, which resets narrowing instead of removing undefined from it.
1629
- /** @type {new () => Record<string, any>} */
1630
- const NullObject = /** @type {any} */ (function () {});
1714
+ const NullObject = /** @type {new () => Record<string, any>} */ (/** @type {unknown} */ (function () {}));
1631
1715
  NullObject.prototype = Object.create(null);
1632
1716
 
1633
1717
  module.exports = {
@@ -1679,6 +1763,9 @@ module.exports = {
1679
1763
  withUtf8Charset,
1680
1764
  asStatError,
1681
1765
  httpError,
1766
+ httpErrorName,
1767
+ headersSentError,
1768
+ applyWriteHead,
1682
1769
  EMPTY_REGEX,
1683
1770
  settingsEpoch
1684
1771
  };
package/src/verify.js CHANGED
@@ -16,16 +16,12 @@ limitations under the License.
16
16
 
17
17
  // npx fulmine verify
18
18
  //
19
- // Whether this machine, and the image it will be deployed in, can run the thing at all. Not
20
- // whether the application behaves the same, which is what the test suite and `differences` are
21
- // for: this is the question that comes before it, and it is the one that costs an hour when the
22
- // answer is no and nobody asked.
19
+ // Whether this machine, and the image it will be deployed in, can run the thing at all. Not whether
20
+ // the application behaves the same, which is the test suite's job.
23
21
  //
24
- // There is a µWebSockets.js binary underneath, and a binary has requirements a package does not:
25
- // it is built per platform, per architecture and per node ABI, and it is linked against glibc. An
26
- // Alpine image, a node version the pinned build has no binary for, a musl base chosen by a
27
- // Dockerfile written before any of this: each one fails at require time, in a container, in CI,
28
- // with a message about a missing module that says nothing about what to do.
22
+ // There is a uWebSockets.js binary underneath, built per platform, per architecture and per node
23
+ // ABI, and linked against glibc. An Alpine image, a node version with no binary for it, a musl
24
+ // base: each one fails at require time with a message about a missing module.
29
25
  //
30
26
  // Thirty seconds here instead.
31
27
 
@@ -34,7 +30,7 @@ limitations under the License.
34
30
  const fs = require("fs");
35
31
  const path = require("path");
36
32
 
37
- // The oldest glibc the pinned µWS binaries are built against. A runtime older than this loads the
33
+ // The oldest glibc the pinned uWS binaries are built against. A runtime older than this loads the
38
34
  // file and then fails on a symbol, which is a worse error than not finding it at all.
39
35
  const MIN_GLIBC = "2.38";
40
36
 
@@ -44,8 +40,8 @@ const MIN_NODE = require("../package.json").engines.node.replace(/[^0-9.]/g, "")
44
40
  const MIN_NODE_MAJOR = MIN_NODE.split(".")[0];
45
41
  const SWAP_IMAGE = `node:${MIN_NODE_MAJOR}-trixie-slim`;
46
42
 
47
- // What a project may carry that needs a different API here rather than none. Everything that just
48
- // works, and everything that only wants a faster built-in, is `npx fulmine migrate`'s business.
43
+ // What a project may carry that needs a different API here. Everything that just works is
44
+ // `npx fulmine migrate`'s business.
49
45
  const NEEDS_A_LOOK = {
50
46
  "socket.io": "attach it with io.attachApp(app.uwsApp), not io.attach(server): there is no node socket to take over",
51
47
  ws: "the websocket server is µWS's own, through app.ws(path, behavior)",
@@ -55,9 +51,8 @@ const NEEDS_A_LOOK = {
55
51
  };
56
52
 
57
53
  /**
58
- * One line of the report. Three levels, and only one of them is a failure: an image that cannot
59
- * load the binary stops the deployment, while a dependency that wants a different call is
60
- * something to read, not something to fail a pipeline over.
54
+ * One line of the report. Only "no" is a failure: an image that cannot load the binary stops the
55
+ * deployment, a dependency that wants a different call does not.
61
56
  *
62
57
  * @param {"ok"|"note"|"no"} level
63
58
  * @param {string} what
@@ -92,9 +87,8 @@ function atLeast(version, minimum) {
92
87
  /**
93
88
  * The node this is running on, against what the package asks for.
94
89
  *
95
- * The version arrives as an argument rather than being read here, so the answer for a node this
96
- * machine is not running is testable from the machine it is not running on. Every check below
97
- * takes what it judges for the same reason.
90
+ * The version is an argument rather than read here, so a node this machine is not running is still
91
+ * testable. Every check below takes what it judges for the same reason.
98
92
  *
99
93
  * @param {string} [running] defaults to the node running this
100
94
  * @param {string} [required] defaults to what package.json asks for
@@ -115,15 +109,15 @@ function checkNode(running = process.versions.node, required = require("../packa
115
109
  * @returns {string|undefined}
116
110
  */
117
111
  function currentGlibc() {
118
- return /** @type {any} */ (process.report.getReport()).header.glibcVersionRuntime;
112
+ return /** @type {{header: {glibcVersionRuntime?: string}}} */ (process.report.getReport()).header
113
+ .glibcVersionRuntime;
119
114
  }
120
115
 
121
116
  /**
122
117
  * Whether the C library is the one the binaries are linked against. Only linux has two of them.
123
118
  *
124
- * Both arguments are required, and deliberately: undefined is the answer that means musl, and a
125
- * default parameter fires on an explicit undefined, so a default here would quietly turn the musl
126
- * case into whatever this machine happens to run. Reading the machine is the caller's job.
119
+ * Both arguments are required on purpose: undefined means musl, and a default parameter fires on
120
+ * an explicit undefined, so a default would turn the musl case into whatever this machine runs.
127
121
  *
128
122
  * @param {string} platform
129
123
  * @param {string|undefined} glibc the runtime glibc, absent on musl
@@ -152,9 +146,8 @@ function checkLibc(platform, glibc) {
152
146
  }
153
147
 
154
148
  /**
155
- * Whether there is a µWebSockets.js binary for this platform, architecture and node ABI, which is
156
- * the failure that greets everyone who tries an unusual combination. The file is named rather than
157
- * loaded first, so the answer says which of the three does not line up.
149
+ * Whether there is a uWebSockets.js binary for this platform, architecture and node ABI. The file
150
+ * is named rather than loaded, so the answer says which of the three does not line up.
158
151
  *
159
152
  * @param {string} [platform]
160
153
  * @param {string} [arch]
@@ -214,12 +207,11 @@ function checkBinary(platform = process.platform, arch = process.arch, abi = pro
214
207
  */
215
208
  function abiToNode(abi) {
216
209
  const known = { 108: "18", 115: "20", 127: "22", 131: "23", 137: "24", 147: "26" };
217
- return /** @type {any} */ (known)[abi] ?? `ABI ${abi}`;
210
+ return known[abi] ?? `ABI ${abi}`;
218
211
  }
219
212
 
220
213
  /**
221
- * The base images a Dockerfile names, which is where the musl question is usually answered without
222
- * anybody meaning to.
214
+ * The base images a Dockerfile names, which is where the musl question is usually answered.
223
215
  *
224
216
  * @param {string} dir the project being verified
225
217
  * @returns {ReturnType<typeof result>[]}
@@ -278,7 +270,7 @@ function checkDependencies(dir) {
278
270
  const installed = { ...pkg.dependencies, ...pkg.devDependencies };
279
271
  for (const name of Object.keys(NEEDS_A_LOOK)) {
280
272
  if (installed[name]) {
281
- results.push(result("note", `${name} needs a different API here`, /** @type {any} */ (NEEDS_A_LOOK)[name]));
273
+ results.push(result("note", `${name} needs a different API here`, NEEDS_A_LOOK[name]));
282
274
  }
283
275
  }
284
276
  return results;
@@ -309,8 +301,7 @@ function verify(argv) {
309
301
  console.log(` ${detail}`);
310
302
  }
311
303
  }
312
- // only a blocked start is a failure. A dependency that wants a different call is worth
313
- // reading and is not worth failing a pipeline over
304
+ // only a blocked start is a failure, a dependency that wants a different call is not
314
305
  const blocking = results.filter((entry) => entry.level === "no").length;
315
306
  const notes = results.filter((entry) => entry.level === "note").length;
316
307
  console.log(
package/src/view.js CHANGED
@@ -73,9 +73,8 @@ module.exports = class View {
73
73
  }
74
74
 
75
75
  /**
76
- * The first of the configured roots that actually holds this template, or undefined when none
77
- * of them does. `views` may be a single directory or a list, and the list is searched in order,
78
- * so an application can put its own templates in front of a package's.
76
+ * The first configured root that holds this template, or undefined. `views` may be one
77
+ * directory or a list, and the list is searched in order.
79
78
  *
80
79
  * @param {string} name template file name, relative to a root
81
80
  * @returns {string|undefined} absolute path to the file that exists
@@ -100,10 +99,9 @@ module.exports = class View {
100
99
  /**
101
100
  * Renders the template through its engine.
102
101
  *
103
- * The callback is always delivered asynchronously, even when the engine answers on the spot.
104
- * `sync` is still true only if the engine called back before this function returned, and in
105
- * that case the callback is pushed to the next tick, so a caller never has to handle both
106
- * orders. Express normalises it the same way.
102
+ * The callback is always delivered asynchronously, even when the engine answers on the spot:
103
+ * `sync` is true only if the engine called back before this returned, and then the callback
104
+ * goes to the next tick. Express does the same.
107
105
  *
108
106
  * @param {Record<string, any>} options locals and engine options, passed through untouched
109
107
  * @param {Function} callback called with whatever the engine passed, which is normally
@@ -115,7 +113,7 @@ module.exports = class View {
115
113
  this.engine(
116
114
  this.path,
117
115
  options,
118
- /** @this {any} */ function onRender() {
116
+ /** @this {unknown} */ function onRender() {
119
117
  if (!sync) {
120
118
  return callback.apply(this, arguments);
121
119
  }