fulmine.js 5.19.3 → 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
@@ -30,6 +30,18 @@ const { Stats } = require("fs");
30
30
 
31
31
  /** @typedef {import("./request.js")} Request */
32
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
+ */
33
45
 
34
46
  const EMPTY_REGEX = new RegExp(``);
35
47
 
@@ -765,8 +777,9 @@ const MEMO_LIMIT = 512;
765
777
  * The wrapped function must never answer undefined, since that is what the cache reads as a miss.
766
778
  * The mime lookups here answer false for something they do not know, which caches correctly.
767
779
  *
768
- * @param {(key: string) => any} fn
769
- * @returns {(key: string) => any}
780
+ * @template T
781
+ * @param {(key: string) => T} fn
782
+ * @returns {(key: string) => T}
770
783
  */
771
784
  function memoizeByString(fn) {
772
785
  const cache = new Map();
@@ -811,7 +824,7 @@ function normalizeType(type) {
811
824
  * unicode escapes, so a string in the body cannot close a script tag in an HTML page that embeds
812
825
  * the response.
813
826
  *
814
- * @param {any} value whatever the handler passed to res.json
827
+ * @param {unknown} value whatever the handler passed to res.json
815
828
  * @param {any} [replacer] the "json replacer" setting
816
829
  * @param {string|number} [spaces] the "json spaces" setting
817
830
  * @param {boolean} [escape] the "json escape" setting
@@ -1042,13 +1055,17 @@ function cachedStat(file, ttl) {
1042
1055
  /**
1043
1056
  * A duration setting as milliseconds: false is off, a string is read by ms, a number is itself.
1044
1057
  *
1045
- * @param {any} value the setting as the application wrote it
1058
+ * @param {string|number|boolean|undefined} value the setting as the application wrote it
1046
1059
  * @param {string} name for the error, which names the setting the application wrote
1047
1060
  * @returns {number}
1048
1061
  */
1049
1062
  function durationSetting(value, name) {
1050
1063
  const parsed =
1051
- 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;
1052
1069
  if (typeof parsed !== "number" || !(parsed >= 0)) {
1053
1070
  throw new TypeError(`${name} must be a duration`);
1054
1071
  }
@@ -1129,8 +1146,9 @@ function deprecated(oldMethod, newMethod, full = false) {
1129
1146
  * request, each time picking up after the route it just ran, and Array.findIndex has no way to
1130
1147
  * start anywhere but the beginning.
1131
1148
  *
1132
- * @param {any[]} arr
1133
- * @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
1134
1152
  * @param {number} [index] where to start
1135
1153
  * @returns {number} the index, or -1
1136
1154
  */
@@ -1165,7 +1183,7 @@ function decode(path) {
1165
1183
  *
1166
1184
  * @param {string} value
1167
1185
  * @returns {string}
1168
- * @throws {any} carrying status 400 when the value cannot be decoded
1186
+ * @throws {HttpError} carrying status 400 when the value cannot be decoded
1169
1187
  */
1170
1188
  function decodeParam(value) {
1171
1189
  // the common case, and worth the check: a parameter is usually a number or a word, and
@@ -1178,7 +1196,8 @@ function decodeParam(value) {
1178
1196
  } catch {
1179
1197
  // a URIError, not an Error: express throws what decodeURIComponent threw, so an error
1180
1198
  // handler written as `err instanceof URIError` has to keep working here
1181
- const err = /** @type {any} */ (new URIError(`Failed to decode param '${value}'`));
1199
+ /** @type {HttpError} */
1200
+ const err = new URIError(`Failed to decode param '${value}'`);
1182
1201
  err.status = 400;
1183
1202
  err.statusCode = 400;
1184
1203
  err.expose = true;
@@ -1335,7 +1354,7 @@ function statTag(stat, weak) {
1335
1354
  * ETag comes from its size and mtime while a body's comes from its contents.
1336
1355
  *
1337
1356
  * @param {{weak: boolean}} options
1338
- * @returns {(body: any, encoding?: BufferEncoding) => string}
1357
+ * @returns {(body: string|Buffer|import("fs").Stats, encoding?: BufferEncoding) => string}
1339
1358
  */
1340
1359
  function createETagGenerator(options) {
1341
1360
  return function generateETag(body, encoding) {
@@ -1362,14 +1381,15 @@ function createETagGenerator(options) {
1362
1381
  * @returns {boolean}
1363
1382
  */
1364
1383
  function isRangeFresh(req, res) {
1365
- 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"]);
1366
1386
  if (!ifRange) {
1367
1387
  return true;
1368
1388
  }
1369
1389
 
1370
1390
  // if-range as etag
1371
1391
  if (ifRange.indexOf('"') !== -1) {
1372
- const etag = res.get("etag");
1392
+ const etag = /** @type {string|undefined} */ (res.get("etag"));
1373
1393
  return Boolean(etag && ifRange.indexOf(etag) !== -1);
1374
1394
  }
1375
1395
 
@@ -1496,16 +1516,73 @@ function headerError(message, code) {
1496
1516
  void err.stack;
1497
1517
  // back to the prototype's "TypeError", which is what node leaves behind. Cast because Error
1498
1518
  // declares name as always present, and this deletes the own property to uncover it again
1499
- delete (/** @type {any} */ (err).name);
1519
+ delete (/** @type {{name?: string}} */ (err).name);
1500
1520
  err.code = code;
1501
1521
  return err;
1502
1522
  }
1503
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
+
1504
1581
  /**
1505
1582
  * Refuses a header name that is not an HTTP token, the way node's setHeader does and with its
1506
1583
  * error, so an application catching ERR_INVALID_HTTP_TOKEN behind Express catches it here.
1507
1584
  *
1508
- * @param {any} name whatever a caller passed as a header name, which is what is being checked
1585
+ * @param {unknown} name whatever a caller passed as a header name, which is what is being checked
1509
1586
  * @returns {void}
1510
1587
  * @throws {TypeError} if the name is not a token, which includes not being a string
1511
1588
  */
@@ -1579,21 +1656,39 @@ const STAT_ERROR_STATUS = { ENAMETOOLONG: 404, ENOTDIR: 404, ENOENT: 404 };
1579
1656
  * of that handler as 500. The message is the status's own name, as http-errors writes it.
1580
1657
  *
1581
1658
  * @param {number} status
1582
- * @returns {any}
1659
+ * @param {string} [message] the status text unless given, as http-errors has it
1660
+ * @returns {HttpError}
1583
1661
  */
1584
- function httpError(status) {
1585
- const message = statuses.message[status] ?? "Error";
1586
- 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);
1587
1665
  // http-errors names these BadRequestError, ForbiddenError and so on, and the name is what the
1588
1666
  // error page shows: an application looking at a 400 sees the same word Express shows it. Set
1589
1667
  // before anything reads the stack, which V8 formats on first read
1590
- err.name = `${message.replace(/\W/g, "")}Error`;
1668
+ err.name = httpErrorName(status);
1591
1669
  err.expose = status < 500;
1592
1670
  err.statusCode = status;
1593
1671
  err.status = status;
1594
1672
  return err;
1595
1673
  }
1596
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
+
1597
1692
  /**
1598
1693
  * Marks an fs error the way send does before it is handed on, so an error handler reading
1599
1694
  * err.status or err.statusCode finds what it would find behind Express. The three properties are
@@ -1602,8 +1697,8 @@ function httpError(status) {
1602
1697
  *
1603
1698
  * The error itself is returned rather than a new one, so its errno, code, syscall and path survive.
1604
1699
  *
1605
- * @param {any} err the fs error, which carries its errno and path
1606
- * @returns {any} the same error
1700
+ * @param {HttpError} err the fs error, which carries its errno and path
1701
+ * @returns {HttpError} the same error
1607
1702
  */
1608
1703
  function asStatError(err) {
1609
1704
  err.expose = false;
@@ -1616,8 +1711,7 @@ function asStatError(err) {
1616
1711
  // A constructor whose instances have no prototype, so a key from a request body or a query string
1617
1712
  // cannot reach Object.prototype. Typed as returning a plain record: without that, assigning one
1618
1713
  // reads as assigning `any`, which resets narrowing instead of removing undefined from it.
1619
- /** @type {new () => Record<string, any>} */
1620
- const NullObject = /** @type {any} */ (function () {});
1714
+ const NullObject = /** @type {new () => Record<string, any>} */ (/** @type {unknown} */ (function () {}));
1621
1715
  NullObject.prototype = Object.create(null);
1622
1716
 
1623
1717
  module.exports = {
@@ -1669,6 +1763,9 @@ module.exports = {
1669
1763
  withUtf8Charset,
1670
1764
  asStatError,
1671
1765
  httpError,
1766
+ httpErrorName,
1767
+ headersSentError,
1768
+ applyWriteHead,
1672
1769
  EMPTY_REGEX,
1673
1770
  settingsEpoch
1674
1771
  };
package/src/verify.js CHANGED
@@ -109,7 +109,8 @@ function checkNode(running = process.versions.node, required = require("../packa
109
109
  * @returns {string|undefined}
110
110
  */
111
111
  function currentGlibc() {
112
- return /** @type {any} */ (process.report.getReport()).header.glibcVersionRuntime;
112
+ return /** @type {{header: {glibcVersionRuntime?: string}}} */ (process.report.getReport()).header
113
+ .glibcVersionRuntime;
113
114
  }
114
115
 
115
116
  /**
@@ -206,7 +207,7 @@ function checkBinary(platform = process.platform, arch = process.arch, abi = pro
206
207
  */
207
208
  function abiToNode(abi) {
208
209
  const known = { 108: "18", 115: "20", 127: "22", 131: "23", 137: "24", 147: "26" };
209
- return /** @type {any} */ (known)[abi] ?? `ABI ${abi}`;
210
+ return known[abi] ?? `ABI ${abi}`;
210
211
  }
211
212
 
212
213
  /**
@@ -269,7 +270,7 @@ function checkDependencies(dir) {
269
270
  const installed = { ...pkg.dependencies, ...pkg.devDependencies };
270
271
  for (const name of Object.keys(NEEDS_A_LOOK)) {
271
272
  if (installed[name]) {
272
- 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]));
273
274
  }
274
275
  }
275
276
  return results;
package/src/view.js CHANGED
@@ -113,7 +113,7 @@ module.exports = class View {
113
113
  this.engine(
114
114
  this.path,
115
115
  options,
116
- /** @this {any} */ function onRender() {
116
+ /** @this {unknown} */ function onRender() {
117
117
  if (!sync) {
118
118
  return callback.apply(this, arguments);
119
119
  }
package/src/walk.js CHANGED
@@ -31,6 +31,7 @@ const {
31
31
  /** @typedef {import("./request.js")} Request */
32
32
  /** @typedef {import("./response.js")} Response */
33
33
  /** @typedef {import("./router.js")} Router */
34
+ /** @typedef {import("./router-utils.js").RouteEntry} RouteEntry */
34
35
 
35
36
  /**
36
37
  * One walk of one router's routes, for one request.
@@ -46,12 +47,12 @@ class Walk {
46
47
  * @param {Router} router
47
48
  * @param {Request} req
48
49
  * @param {Response} res
49
- * @param {any[]} routes the route table being walked, see createRoute in router.js
50
+ * @param {RouteEntry[]} routes the route table being walked, see createRoute in router.js
50
51
  * @param {boolean} skipCheck take the route at the index without matching it, which is how an
51
52
  * already-decided chain is walked
52
- * @param {any} skipUntil route to resume after when this chain runs out, or undefined
53
- * @param {(value: any) => void} resolve
54
- * @param {(err: any) => void} reject
53
+ * @param {RouteEntry|undefined} skipUntil route to resume after when this chain runs out, or undefined
54
+ * @param {(value: RouteEntry|false) => void} resolve
55
+ * @param {(err: unknown) => void} reject
55
56
  */
56
57
  constructor(router, req, res, routes, skipCheck, skipUntil, resolve, reject) {
57
58
  this.router = router;
@@ -85,7 +86,7 @@ class Walk {
85
86
  * Leaves the rest of this route, with the error if there is one, and carries on with the route
86
87
  * after it.
87
88
  *
88
- * @param {any} [err] whatever was thrown, which need not be an Error
89
+ * @param {unknown} [err] whatever was thrown, which need not be an Error
89
90
  */
90
91
  stepOutOfRoute(err) {
91
92
  if (err) {
@@ -289,7 +290,7 @@ class Walk {
289
290
  * Enters the route the walk is on: a mount adjusts req.url, req.path and the mount stack on the
290
291
  * way in, and then the route's callbacks run one after another through next().
291
292
  *
292
- * @param {any} continueRoute what _preprocessRequest decided: true to run, "route" to skip
293
+ * @param {true|"route"} continueRoute what _preprocessRequest decided: true to run, "route" to skip
293
294
  */
294
295
  runRoute(continueRoute) {
295
296
  const req = this.req;
@@ -439,7 +440,7 @@ class Walk {
439
440
  * One hop, which is what next() does: with nothing, run the route's next callback; with "route",
440
441
  * leave the route; with anything else, remember it as the error and carry on.
441
442
  *
442
- * @param {any} thingamabob what next() was called with: nothing, "route", or an error
443
+ * @param {unknown} thingamabob what next() was called with: nothing, "route", or an error
443
444
  */
444
445
  step(thingamabob) {
445
446
  const req = this.req;
package/src/websocket.js CHANGED
@@ -20,6 +20,12 @@ const { canBeOptimizedWithParams, decodeParam, NullObject } = require("./utils.j
20
20
 
21
21
  /** @typedef {import("./router.js")} Router */
22
22
  /** @typedef {import("./application.js").Application} Application */
23
+ /** @typedef {import("./request.js")} Request */
24
+ /** @typedef {import("./response.js")} Response */
25
+ /** @typedef {import("./router-utils.js").WsRoute} WsRoute */
26
+ /** @typedef {import("uWebSockets.js").HttpRequest} UwsRequest */
27
+ /** @typedef {import("uWebSockets.js").HttpResponse} UwsResponse */
28
+ /** @typedef {import("uWebSockets.js").us_socket_context_t} UwsContext */
23
29
 
24
30
  // the parameter names in a path, in the order µWS numbers them
25
31
  const PARAM = /:(\w+)/g;
@@ -55,8 +61,8 @@ function joinPaths(prefix, path) {
55
61
  * @param {Router} router
56
62
  * @param {string|null} prefix the mount path accumulated so far, or null once a mount was a
57
63
  * shape µWS cannot match, which makes everything below it unreachable
58
- * @param {any[]} out
59
- * @param {Set<any>} seen routers already walked, since a router may be mounted twice
64
+ * @param {WsRoute[]} out
65
+ * @param {Set<Router>} seen routers already walked, since a router may be mounted twice
60
66
  */
61
67
  function collectRoutes(router, prefix, out, seen) {
62
68
  if (seen.has(router)) {
@@ -102,14 +108,17 @@ function collectRoutes(router, prefix, out, seen) {
102
108
  * The uWS upgrade handler for one route: builds this project's request and response, offers them
103
109
  * to the application's own `upgrade` hook, and completes the handshake unless that hook answered.
104
110
  *
105
- * @param {Application} app the application whose request and response classes serve this route
111
+ * @param {Router} app the router whose request and response classes serve this route
106
112
  * @param {string} path the composed path, whose parameters are read back by index
107
- * @param {any} behavior what the caller registered
108
- * @returns {(res: any, req: any, context: any) => void}
113
+ * @param {Record<string, unknown>} behavior what the caller registered
114
+ * @returns {(res: UwsResponse, req: UwsRequest, context: UwsContext) => void}
109
115
  */
110
116
  function makeUpgradeHandler(app, path, behavior) {
111
117
  const paramNames = [...path.matchAll(PARAM)].map((match) => match[1]);
112
- const userUpgrade = behavior.upgrade;
118
+ // a function, checked by checkBehavior where it was registered
119
+ const userUpgrade = /** @type {((req: Request, res: Response) => void|Promise<void>)|undefined} */ (
120
+ behavior.upgrade
121
+ );
113
122
 
114
123
  return (res, req, context) => {
115
124
  // read off the uWS request before anything can await: it is neutered on return, and the
@@ -122,7 +131,7 @@ function makeUpgradeHandler(app, path, behavior) {
122
131
  if (paramNames.length) {
123
132
  const params = new NullObject();
124
133
  for (let i = 0; i < paramNames.length; i++) {
125
- params[paramNames[i]] = decodeParam(req.getParameter(i));
134
+ params[paramNames[i]] = decodeParam(/** @type {string} */ (req.getParameter(i)));
126
135
  }
127
136
  request.params = params;
128
137
  }
@@ -216,7 +225,7 @@ function registerWebSocketRoutes(app) {
216
225
  * used: a handler under a misspelled name would otherwise never run and never say why.
217
226
  *
218
227
  * @param {string} path
219
- * @param {any} behavior uWS's WebSocketBehavior, whose shipped typings do not describe it
228
+ * @param {unknown} behavior whatever was passed as one, which is what is being checked
220
229
  */
221
230
  function checkBehavior(path, behavior) {
222
231
  if (typeof path !== "string") {
package/src/work.js CHANGED
@@ -54,7 +54,7 @@ function work(req, res) {
54
54
  const native = req.route?._native;
55
55
  // cast for the three the classes do not declare: `body` is deliberately not a field of
56
56
  // Request, and the two stream states are node's own, written when a lazy stream is built
57
- const loose = /** @type {any} */ (req);
57
+ const loose = /** @type {{body?: unknown, _readableState?: unknown}} */ (req);
58
58
  return {
59
59
  native: Boolean(native),
60
60
  declarative: Boolean(native?.declarative),
@@ -62,7 +62,7 @@ function work(req, res) {
62
62
  query: req._queryParsed,
63
63
  body: loose.body !== undefined,
64
64
  requestStream: loose._readableState !== undefined,
65
- responseStream: /** @type {any} */ (res)._writableState !== undefined,
65
+ responseStream: /** @type {{_writableState?: unknown}} */ (res)._writableState !== undefined,
66
66
  socket: req._socketBuilt || res._socketBuilt
67
67
  };
68
68
  }
@@ -86,7 +86,7 @@ const NAMES = [
86
86
  function names(done) {
87
87
  const listed = [];
88
88
  for (const [key, name] of NAMES) {
89
- if (/** @type {any} */ (done)[key]) {
89
+ if (done[key]) {
90
90
  listed.push(name);
91
91
  }
92
92
  }