fulmine.js 5.19.8 → 5.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/utils.js CHANGED
@@ -208,6 +208,9 @@ function getPatternMeta(pattern) {
208
208
  * A bare `*`, an unnamed parameter, an inline regex like :id(\\d+) and the `+`, `?`, `()` operators
209
209
  * throw: a route that quietly stops matching is worse than one that fails at startup. The names it
210
210
  * captures go in a WeakMap beside the regex, see PatternMeta.
211
+ *
212
+ * @param {string|RegExp} pattern
213
+ * @returns {RegExp}
211
214
  */
212
215
  function patternToRegex(pattern, isPrefix = false, caseSensitive = true, strict = false) {
213
216
  if (pattern instanceof RegExp) {
@@ -222,11 +225,12 @@ function patternToRegex(pattern, isPrefix = false, caseSensitive = true, strict
222
225
  let regexPattern = "";
223
226
  let i = 0;
224
227
  const len = pattern.length;
225
- const wildcardNames = [];
228
+ const wildcardNames = /** @type {string[]} */ ([]);
226
229
  // express takes /:a/:a, and two capture groups cannot share a name, so a repeat is compiled
227
230
  // under a spelling of its own and mapped back when the parameters are read out. Reading them
228
231
  // in order then leaves the last occurrence in place, which is the value express reports
229
- const groupOutputName = new Map();
232
+ const groupOutputName = /** @type {Map<string, string>} */ (new Map());
233
+ /** @param {string} name */
230
234
  const uniqueGroupName = (name) => {
231
235
  if (!groupOutputName.has(name)) {
232
236
  groupOutputName.set(name, name);
@@ -255,7 +259,11 @@ function patternToRegex(pattern, isPrefix = false, caseSensitive = true, strict
255
259
  let lastCaptureWasWildcard = false;
256
260
  let wildcardInSegment = false;
257
261
  let paramInSegment = false;
258
- /** Records literal text as it is emitted, which is what the rules above are written against. */
262
+ /**
263
+ * Records literal text as it is emitted, which is what the rules above are written against.
264
+ *
265
+ * @param {string} text
266
+ */
259
267
  const literal = (text) => {
260
268
  backtrack += text;
261
269
  if (lastCaptureWasWildcard) {
@@ -750,7 +758,8 @@ function acceptParams(str) {
750
758
  const length = str.length;
751
759
  const colonIndex = str.indexOf(";");
752
760
  let index = colonIndex === -1 ? length : colonIndex;
753
- const ret = { value: str.slice(0, index).trim(), quality: 1, params: {} };
761
+ const params = /** @type {Record<string, string>} */ ({});
762
+ const ret = { value: str.slice(0, index).trim(), quality: 1, params };
754
763
 
755
764
  while (index < length) {
756
765
  const splitIndex = str.indexOf("=", index);
@@ -1098,13 +1107,20 @@ function durationSetting(value, name) {
1098
1107
  return parsed;
1099
1108
  }
1100
1109
 
1110
+ /**
1111
+ * The predicate "trust proxy" compiles to: whether the address at hop i is trusted. The address is
1112
+ * undefined over a unix socket, see Request#parsedIp.
1113
+ *
1114
+ * @typedef {(addr: string|undefined, i: number) => boolean} TrustFn
1115
+ */
1116
+
1101
1117
  /**
1102
1118
  * Turns whatever "trust proxy" was set to into the function proxy-addr wants: a predicate saying
1103
1119
  * whether the address at hop i is trusted. true trusts everything, a number trusts that many hops,
1104
1120
  * and a string or a list is read as addresses and subnet names.
1105
1121
  *
1106
- * @param {boolean|number|string|string[]|Function} val
1107
- * @returns {Function}
1122
+ * @param {boolean|number|string|string[]|TrustFn} val
1123
+ * @returns {TrustFn}
1108
1124
  */
1109
1125
  function compileTrust(val) {
1110
1126
  if (typeof val === "function") return val;
@@ -1118,8 +1134,9 @@ function compileTrust(val) {
1118
1134
 
1119
1135
  if (typeof val === "number") {
1120
1136
  // Support trusting hop count
1121
- return function (a, i) {
1122
- return i < val;
1137
+ const hops = val;
1138
+ return function (/** @type {string|undefined} */ a, /** @type {number} */ i) {
1139
+ return i < hops;
1123
1140
  };
1124
1141
  }
1125
1142
 
@@ -1130,7 +1147,9 @@ function compileTrust(val) {
1130
1147
  });
1131
1148
  }
1132
1149
 
1133
- return proxyaddr.compile(val || []);
1150
+ // proxy-addr answers false to an address it cannot parse, undefined included, though its
1151
+ // typing does not admit one
1152
+ return /** @type {TrustFn} */ (proxyaddr.compile(val || []));
1134
1153
  }
1135
1154
 
1136
1155
  const shownWarnings = new Set();
package/src/verify.js CHANGED
@@ -29,6 +29,7 @@ limitations under the License.
29
29
 
30
30
  const fs = require("fs");
31
31
  const path = require("path");
32
+ const { detectManager, UWS_SPEC, UWS_OVERRIDE } = require("./adopt.js");
32
33
 
33
34
  // The oldest glibc the pinned uWS binaries are built against. A runtime older than this loads the
34
35
  // file and then fails on a symbol, which is a worse error than not finding it at all.
@@ -251,6 +252,20 @@ function checkDockerfiles(dir) {
251
252
  return results;
252
253
  }
253
254
 
255
+ /**
256
+ * The project's package.json, or nothing where there is none to read.
257
+ *
258
+ * @param {string} dir
259
+ * @returns {any}
260
+ */
261
+ function readPackage(dir) {
262
+ try {
263
+ return JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf8"));
264
+ } catch {
265
+ return undefined;
266
+ }
267
+ }
268
+
254
269
  /**
255
270
  * The dependencies that need a different API here. Read from package.json rather than from
256
271
  * node_modules, so a project is answered before it installs anything.
@@ -261,12 +276,8 @@ function checkDockerfiles(dir) {
261
276
  function checkDependencies(dir) {
262
277
  /** @type {ReturnType<typeof result>[]} */
263
278
  const results = [];
264
- let pkg;
265
- try {
266
- pkg = JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf8"));
267
- } catch {
268
- return results;
269
- }
279
+ const pkg = readPackage(dir);
280
+ if (!pkg) return results;
270
281
  const installed = { ...pkg.dependencies, ...pkg.devDependencies };
271
282
  for (const name of Object.keys(NEEDS_A_LOOK)) {
272
283
  if (installed[name]) {
@@ -276,6 +287,83 @@ function checkDependencies(dir) {
276
287
  return results;
277
288
  }
278
289
 
290
+ // pnpm refuses a git dependency of a dependency since this version, and µWebSockets.js is one
291
+ const PNPM_BLOCKS_GIT_SUBDEPS = [10, 26];
292
+
293
+ /**
294
+ * Whether the package manager will install this at all. pnpm 10.26 and later refuse a dependency
295
+ * of a dependency that comes from git, which µWebSockets.js does, so `pnpm add fulmine.js` fails
296
+ * before anything runs. What lets it through is readable from the project: the two lines
297
+ * `npx fulmine.js pnpm` writes, the setting turned off, or an override taking µWebSockets.js from
298
+ * a registry. pnpm 11 reads its settings from pnpm-workspace.yaml only, so that is what is read.
299
+ *
300
+ * @param {string} dir
301
+ * @param {any} pkg the parsed package.json
302
+ * @returns {ReturnType<typeof result>|undefined} nothing to say for npm and yarn
303
+ */
304
+ function checkPackageManager(dir, pkg) {
305
+ const { manager, why } = detectManager(dir, pkg);
306
+ if (manager !== "pnpm") return undefined;
307
+
308
+ const declared = /^pnpm@(\d+)\.(\d+)/.exec(pkg.packageManager ?? "");
309
+ if (declared) {
310
+ const [major, minor] = [Number(declared[1]), Number(declared[2])];
311
+ const [blockMajor, blockMinor] = PNPM_BLOCKS_GIT_SUBDEPS;
312
+ if (major < blockMajor || (major === blockMajor && minor < blockMinor)) {
313
+ return result("ok", `pnpm ${declared[1]}.${declared[2]} installs a git dependency of a dependency`);
314
+ }
315
+ }
316
+
317
+ let workspace = "";
318
+ try {
319
+ workspace = fs.readFileSync(path.join(dir, "pnpm-workspace.yaml"), "utf8");
320
+ } catch {
321
+ // no workspace file, so every setting is at its default
322
+ }
323
+
324
+ // the recipe: the project owns the git dependency, and the copy this package asks for is dropped
325
+ const dropped = new RegExp(`^\\s*["']?${UWS_OVERRIDE.replace(/\./g, "\\.")}["']?:\\s*["']?-["']?\\s*$`, "m").test(
326
+ workspace
327
+ );
328
+ const own = pkg.dependencies?.["uWebSockets.js"];
329
+ if (dropped && typeof own === "string") {
330
+ if (own === UWS_SPEC) {
331
+ return result(
332
+ "ok",
333
+ "pnpm, with µWebSockets.js as the project's own dependency at the pin this package uses"
334
+ );
335
+ }
336
+ return result(
337
+ "note",
338
+ `pnpm, with µWebSockets.js as the project's own dependency at ${own}`,
339
+ `this package pins ${UWS_SPEC} and was tested against it. \`npx fulmine.js pnpm\` writes the pin.`
340
+ );
341
+ }
342
+ if (dropped) {
343
+ return result(
344
+ "no",
345
+ "pnpm-workspace.yaml drops µWebSockets.js from this package, and the project does not declare it",
346
+ `nothing would install it. \`npx fulmine.js pnpm\` adds "uWebSockets.js": "${UWS_SPEC}" to dependencies.`
347
+ );
348
+ }
349
+
350
+ if (/^\s*blockExoticSubdeps:\s*false\s*$/m.test(workspace)) {
351
+ return result("ok", "pnpm, with blockExoticSubdeps off in pnpm-workspace.yaml");
352
+ }
353
+ const registry = /^\s*["']?uWebSockets\.js["']?:\s*["']?([~^]?\d[^"'\s]*)/m.exec(workspace);
354
+ if (registry) {
355
+ return result("ok", `pnpm, with µWebSockets.js overridden to ${registry[1]} from a registry`);
356
+ }
357
+
358
+ return result(
359
+ "no",
360
+ `pnpm (${why}) will refuse to install this`,
361
+ "pnpm 10.26 and later block a git dependency of a dependency, and µWebSockets.js is one:\n" +
362
+ " pnpm add fulmine.js fails with ERR_PNPM_EXOTIC_SUBDEP. `npx fulmine.js pnpm` writes the two\n" +
363
+ " lines that let it through, see docs/deployment.md."
364
+ );
365
+ }
366
+
279
367
  /**
280
368
  * Runs every check and prints the report. Anything that would stop the application from starting
281
369
  * is a failure and the command exits non-zero, so it can be a step in a pipeline.
@@ -292,6 +380,10 @@ function verify(argv) {
292
380
  results.push(libc);
293
381
  }
294
382
  results.push(checkBinary(), ...checkDockerfiles(dir), ...checkDependencies(dir));
383
+ const manager = checkPackageManager(dir, readPackage(dir) ?? {});
384
+ if (manager) {
385
+ results.push(manager);
386
+ }
295
387
 
296
388
  console.log(`\nWhether this machine and this project can run fulmine.js\n`);
297
389
  const label = { ok: "ok ", note: "note", no: "NO " };
@@ -312,4 +404,13 @@ function verify(argv) {
312
404
  return blocking === 0 ? 0 : 1;
313
405
  }
314
406
 
315
- module.exports = { verify, checkNode, checkLibc, currentGlibc, checkBinary, checkDockerfiles, checkDependencies };
407
+ module.exports = {
408
+ verify,
409
+ checkNode,
410
+ checkLibc,
411
+ currentGlibc,
412
+ checkBinary,
413
+ checkDockerfiles,
414
+ checkDependencies,
415
+ checkPackageManager
416
+ };
package/src/walk.js CHANGED
@@ -79,6 +79,7 @@ class Walk {
79
79
  //
80
80
  // Null here and bound on the first route with more than one callback, the only shape that
81
81
  // reads it: a request that never meets one paid a bind for nothing
82
+ /** @type {((err?: unknown) => void)|null} */
82
83
  this.leaveRoute = null;
83
84
  }
84
85
 
@@ -103,6 +104,7 @@ class Walk {
103
104
  * a chain of N middlewares costs one promise instead of N nested ones.
104
105
  *
105
106
  * @param {number} startIndex where to resume the scan
107
+ * @returns {void}
106
108
  */
107
109
  dispatch(startIndex) {
108
110
  const req = this.req;
@@ -226,7 +228,7 @@ class Walk {
226
228
  .then((resumed) => this.runRoute(resumed))
227
229
  // wrapped so the native pair keeps the walk as receiver; a promise's reject
228
230
  // would not have cared
229
- .catch((err) => this.reject(err));
231
+ .catch((/** @type {unknown} */ err) => this.reject(err));
230
232
  return;
231
233
  }
232
234
  return this.runRoute(continueRoute);
@@ -291,6 +293,7 @@ class Walk {
291
293
  * way in, and then the route's callbacks run one after another through next().
292
294
  *
293
295
  * @param {true|"route"} continueRoute what _preprocessRequest decided: true to run, "route" to skip
296
+ * @returns {void}
294
297
  */
295
298
  runRoute(continueRoute) {
296
299
  const req = this.req;
@@ -347,6 +350,7 @@ class Walk {
347
350
  *
348
351
  * @param {number} kind what the callback is, one of the CALLBACK_ constants
349
352
  * @param {Function} callback
353
+ * @returns {void}
350
354
  */
351
355
  errorHop(kind, callback) {
352
356
  const req = this.req;
@@ -441,6 +445,7 @@ class Walk {
441
445
  * leave the route; with anything else, remember it as the error and carry on.
442
446
  *
443
447
  * @param {unknown} thingamabob what next() was called with: nothing, "route", or an error
448
+ * @returns {void}
444
449
  */
445
450
  step(thingamabob) {
446
451
  const req = this.req;
@@ -492,7 +497,7 @@ class Walk {
492
497
  }
493
498
  callback
494
499
  ._routeRequest(req, res, 0)
495
- .then((routed) => {
500
+ .then((/** @type {RouteEntry|false} */ routed) => {
496
501
  // the child's params are scoped to it, and must not leak into the routes after
497
502
  if (pushedParams) {
498
503
  req._paramStack.pop();
@@ -531,7 +536,7 @@ class Walk {
531
536
  // a rejection out of the nested walk, or a throw above, must reject this one
532
537
  // instead of dying as an unhandled rejection; wrapped for the native pair's
533
538
  // receiver
534
- .catch((err) => this.reject(err));
539
+ .catch((/** @type {unknown} */ err) => this.reject(err));
535
540
  } else {
536
541
  // errors and error handlers live out of line: this is the cold path, and its size
537
542
  // was pushing step past the inlining threshold
package/src/websocket.js CHANGED
@@ -208,7 +208,7 @@ function makeUpgradeHandler(app, path, behavior) {
208
208
  * @param {Application} app
209
209
  */
210
210
  function registerWebSocketRoutes(app) {
211
- const routes = [];
211
+ const routes = /** @type {WsRoute[]} */ ([]);
212
212
  collectRoutes(app, "", routes, new Set());
213
213
  for (const route of routes) {
214
214
  const uwsBehavior = { ...route.behavior };