fulmine.js 5.19.3 → 5.19.5

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.
@@ -18,13 +18,15 @@ limitations under the License.
18
18
  */
19
19
 
20
20
  const acorn = require("acorn");
21
- const { stringify, withDefaultCharset, withUtf8Charset, contentTypeFor } = require("./utils.js");
21
+ const { stringify, contentTypeSet, withUtf8Charset, contentTypeFor } = require("./utils.js");
22
22
  // H3App, DeclarativeResponse and _cfg exist at runtime but are missing from the .d.ts the
23
23
  // package ships, so the module is read through a loose alias
24
24
  const uWS = require("uWebSockets.js");
25
25
  const uWSAny = /** @type {any} */ (uWS);
26
26
  const statuses = require("statuses");
27
27
 
28
+ /** @typedef {import("./application.js").Application} Application */
29
+
28
30
  const parser = acorn.Parser;
29
31
 
30
32
  const allowedResMethods = [
@@ -89,7 +91,8 @@ const understoodNodeTypes = new Set([
89
91
  * Every node type in the tree. Walks all the keys instead of named edges, so the answer does not
90
92
  * depend on the walk being complete.
91
93
  *
92
- * @param {any} node an acorn AST node. acorn ships no useful node types, and every shape here is checked by hand
94
+ * @param {any} node an acorn node, or an array or a scalar under one: walked by key, so no shape
95
+ * is assumed
93
96
  * @param {Set<string>} types
94
97
  */
95
98
  function collectNodeTypes(node, types) {
@@ -112,7 +115,7 @@ function collectNodeTypes(node, types) {
112
115
  /**
113
116
  * The key a property writes. Only a plain name or a literal, never computed, a getter or a spread.
114
117
  *
115
- * @param {any} property an acorn Property node
118
+ * @param {import("acorn").AnyNode} property an acorn node, a Property when it is one this reads
116
119
  * @returns {string|null} null when the shape is not one of those
117
120
  */
118
121
  function literalKeyOf(property) {
@@ -129,8 +132,8 @@ function literalKeyOf(property) {
129
132
  * The value of a literal expression, for the shapes known at registration time. Anything else
130
133
  * throws, and the catch around the compiler turns it into ordinary routing.
131
134
  *
132
- * @param {any} node an acorn AST node. acorn ships no useful node types, and every shape here is checked by hand
133
- * @returns {any} whatever the literal denotes
135
+ * @param {import("acorn").AnyNode} node
136
+ * @returns {unknown} whatever the literal denotes
134
137
  */
135
138
  function literalValue(node) {
136
139
  switch (node.type) {
@@ -187,8 +190,9 @@ function literalValue(node) {
187
190
  * The status and the headers the calls set, in the order they were first written. null when one
188
191
  * of them is not a literal this can read.
189
192
  *
190
- * @param {any[]} callExprs the res calls, in run order
191
- * @param {any[]} headers written to, so the caller keeps the array the body reader also uses
193
+ * @param {any[]} callExprs the res calls, in run order, each carrying what readResCalls read off
194
+ * its callee as `obj`; loose because the arguments are taken as whatever literal they hold
195
+ * @param {[string, string][]} headers written to, so the caller keeps the array the body reader also uses
192
196
  * @returns {{statusCode: number, sendStatusUsed: boolean}|null}
193
197
  */
194
198
  function readStatusAndHeaders(callExprs, headers) {
@@ -246,10 +250,16 @@ function readStatusAndHeaders(callExprs, headers) {
246
250
 
247
251
  for (let [header, value] of pairs) {
248
252
  const name = String(header).toLowerCase();
249
- // res.set adds a charset to a content-type, res.setHeader does not: setHeader
250
- // is node's and node does not know what a media type is
253
+ // res.set resolves a content-type through the mime database, res.setHeader does
254
+ // not: setHeader is node's and node does not know what a media type is
251
255
  if (call.obj.propertyName !== "setHeader" && name === "content-type") {
252
- value = withDefaultCharset(value);
256
+ const resolved = contentTypeSet(String(value));
257
+ if (resolved === false) {
258
+ // res.set stores false for it and the body method writes its own type
259
+ // instead; left to the ordinary path rather than worked out twice here
260
+ return null;
261
+ }
262
+ value = resolved;
253
263
  }
254
264
  const index = headers.findIndex((entry) => String(entry[0]).toLowerCase() === name);
255
265
  if (index === -1) {
@@ -286,10 +296,10 @@ function readStatusAndHeaders(callExprs, headers) {
286
296
  * The body parts the calls write, pushed into `body`, with the content-type decisions they imply
287
297
  * pushed into `headers`. null when one of the calls writes something this cannot read.
288
298
  *
289
- * @param {any[]} callExprs the res calls, in run order
290
- * @param {any[]} headers the headers read so far, written to
291
- * @param {any[]} body the body parts, written to
292
- * @param {any} app the application, for the json settings
299
+ * @param {any[]} callExprs the res calls, in run order, as readStatusAndHeaders takes them
300
+ * @param {[string, string][]} headers the headers read so far, written to
301
+ * @param {any[]} body the body parts, written to; loose because a literal's value is kept as it is
302
+ * @param {Application} app the application, for the json settings
293
303
  * @param {string[]} queries names bound by a destructured req.query
294
304
  * @param {string[]} params names bound by a destructured req.params
295
305
  * @returns {{sendUsed: boolean, bodyFromSend: boolean}|null}
@@ -310,6 +320,11 @@ function readBody(callExprs, headers, body, app, queries, params) {
310
320
  if (call.obj.propertyName !== "end") {
311
321
  bodyFromSend = true;
312
322
  }
323
+ // one argument at most: res.end(data, encoding) and res.end(data, cb) are shapes a
324
+ // compiled response cannot stand for, and stood for them as if the extra were not there
325
+ if (call.arguments.length > 1) {
326
+ return null;
327
+ }
313
328
  const arg = call.arguments[0];
314
329
 
315
330
  if (call.obj.propertyName === "json") {
@@ -420,7 +435,8 @@ function readBody(callExprs, headers, body, app, queries, params) {
420
435
  * Reads a chain of string concatenations right to left. Each side must be a literal or a
421
436
  * param or query value, anything else makes the whole handler fall back.
422
437
  *
423
- * @param {any} node a BinaryExpression
438
+ * @param {any} node a BinaryExpression, read loosely: the literal on either
439
+ * side is kept as it is
424
440
  * @returns {boolean}
425
441
  */
426
442
  function check(node) {
@@ -479,7 +495,7 @@ function readBody(callExprs, headers, body, app, queries, params) {
479
495
  * through, too few parameters, or a return that is not the last statement.
480
496
  *
481
497
  * @param {Function} cb
482
- * @returns {{fn: any, args: string[]}|null}
498
+ * @returns {{fn: import("acorn").FunctionDeclaration|import("acorn").ArrowFunctionExpression, args: string[]}|null}
483
499
  */
484
500
  function readHandler(cb) {
485
501
  let code = cb.toString();
@@ -491,7 +507,7 @@ function readHandler(cb) {
491
507
  // Anything not understood returns false and falls back to ordinary routing. Widening the
492
508
  // list below is not worth it: over the 1113 handlers in tests, demo and benchmark, 42.6%
493
509
  // call something that is not res, `const` would unlock 7 (0.6%), a conditional 0.1% more.
494
- /** @type {any[]} */
510
+ /** @type {any[]} the tokens, loose because acorn's Token type leaves out value */
495
511
  const tokens = [...acorn.tokenizer(code, { ecmaVersion: "latest" })];
496
512
 
497
513
  if (
@@ -520,7 +536,7 @@ function readHandler(cb) {
520
536
  return null;
521
537
  }
522
538
 
523
- /** @type {any[]} */
539
+ /** @type {any[]} the statements, read loosely: what a parameter may be is checked by hand in readParamNames */
524
540
  const parsed = parser.parse(code, { ecmaVersion: "latest" }).body;
525
541
  let fn = parsed[0];
526
542
 
@@ -563,14 +579,25 @@ function readHandler(cb) {
563
579
  return { fn, args };
564
580
  }
565
581
 
582
+ /**
583
+ * What readParamNames found: the two parameter names, and what a destructured req bound.
584
+ * @typedef {object} ParamNames
585
+ * @property {string} req
586
+ * @property {string} res
587
+ * @property {string|undefined} queryName
588
+ * @property {string|undefined} paramsName
589
+ * @property {string[]} queries
590
+ * @property {string[]} params
591
+ */
592
+
566
593
  /**
567
594
  * The names a destructured `req` binds for query and params, so the body reader can tell one of
568
595
  * them from an identifier it must refuse. null when the pattern is one this cannot read.
569
596
  *
570
- * @param {any} fn the handler's AST
597
+ * @param {any} fn the handler's AST, read loosely: a destructured parameter is checked shape by
598
+ * shape, and anything else throws into the fallback
571
599
  * @param {string[]} args its parameter names
572
- * @returns {{req: string, res: string, queryName: string|undefined, paramsName: string|undefined,
573
- * queries: string[], params: string[]}|null}
600
+ * @returns {ParamNames|null}
574
601
  */
575
602
  function readParamNames(fn, args) {
576
603
  const [req, res] = args;
@@ -615,9 +642,10 @@ function readParamNames(fn, args) {
615
642
  * Every call the handler makes, in the order they run, cut after the one that writes the body.
616
643
  * null when it calls anything but `res`, or a method a compiled response cannot stand for.
617
644
  *
618
- * @param {any} fn the handler's AST
645
+ * @param {import("acorn").FunctionDeclaration|import("acorn").ArrowFunctionExpression} fn the handler's AST
619
646
  * @param {string} res the name its second parameter was given
620
- * @returns {any[]|null}
647
+ * @returns {any[]|null} the call nodes, each carrying what was read off its callee as `obj`; loose
648
+ * because the readers take their arguments as whatever literal they hold
621
649
  */
622
650
  function readResCalls(fn, res) {
623
651
  // check if it calls any other function other than the one in `res`
@@ -698,9 +726,9 @@ function readResCalls(fn, res) {
698
726
  /**
699
727
  * Whether every identifier in the handler is one a compiled response can stand for.
700
728
  *
701
- * @param {any} fn the handler's AST
729
+ * @param {import("acorn").FunctionDeclaration|import("acorn").ArrowFunctionExpression} fn the handler's AST
702
730
  * @param {string[]} args its parameter names
703
- * @param {any} names what a destructured req bound, from readParamNames
731
+ * @param {ParamNames} names what a destructured req bound, from readParamNames
704
732
  * @returns {boolean}
705
733
  */
706
734
  function identifiersAllowed(fn, args, names) {
@@ -801,10 +829,10 @@ module.exports = function compileDeclarative(cb, app) {
801
829
  if (!connection && advertise) {
802
830
  decRes = decRes.writeHeader("connection", "keep-alive");
803
831
  }
804
- // not when the handler is closing: Keep-Alive describes a connection that stays open, and
805
- // the ordinary path leaves it out for the same reason
806
- const closing = typeof connection?.[1] === "string" && connection[1].toLowerCase() === "close";
807
- if (advertise && !closing && !headers.some((header) => header[0].toLowerCase() === "keep-alive")) {
832
+ // not when the route wrote its own Connection, whatever it says: node writes the two as a
833
+ // pair and writes neither once the response has set Connection, and the ordinary path
834
+ // leaves it out for the same reason
835
+ if (advertise && !connection && !headers.some((header) => header[0].toLowerCase() === "keep-alive")) {
808
836
  decRes = decRes.writeHeader("keep-alive", "timeout=10");
809
837
  }
810
838
 
@@ -869,9 +897,10 @@ module.exports = function compileDeclarative(cb, app) {
869
897
  * Every node matching the predicate, in the order of the named edges below. The edges are written
870
898
  * by hand, which is why compileDeclarative first refuses any node type not on the understood list.
871
899
  *
872
- * @param {any} node an acorn AST node. acorn ships no useful node types, and every shape here is checked by hand
873
- * @param {(node: any) => boolean} fn
874
- * @returns {any[]}
900
+ * @param {any} node an acorn node, walked along the named edges below, so nothing is assumed
901
+ * about its shape
902
+ * @param {(node: import("acorn").AnyNode) => boolean} fn
903
+ * @returns {any[]} the matching nodes, as loose as the input
875
904
  */
876
905
  function filterNodes(node, fn) {
877
906
  const filtered = [];
@@ -47,9 +47,9 @@ class HotSettings {
47
47
  */
48
48
  const settingsWriteTraps = {
49
49
  /**
50
- * @param {any} target the settings object itself, whose keys are the application's
50
+ * @param {Record<string|symbol, unknown>} target the settings object itself, whose keys are the application's
51
51
  * @param {string|symbol} key
52
- * @param {any} value whatever the application is setting
52
+ * @param {unknown} value whatever the application is setting
53
53
  */
54
54
  set(target, key, value) {
55
55
  target[key] = value;
@@ -57,7 +57,7 @@ const settingsWriteTraps = {
57
57
  return true;
58
58
  },
59
59
  /**
60
- * @param {any} target the settings object itself
60
+ * @param {Record<string|symbol, unknown>} target the settings object itself
61
61
  * @param {string|symbol} key
62
62
  */
63
63
  deleteProperty(target, key) {
@@ -66,9 +66,9 @@ const settingsWriteTraps = {
66
66
  return true;
67
67
  },
68
68
  /**
69
- * @param {any} target the settings object itself
69
+ * @param {Record<string|symbol, unknown>} target the settings object itself
70
70
  * @param {string|symbol} key
71
- * @param {any} descriptor
71
+ * @param {PropertyDescriptor} descriptor
72
72
  */
73
73
  defineProperty(target, key, descriptor) {
74
74
  Object.defineProperty(target, key, descriptor);
@@ -74,7 +74,7 @@ for (const member of [
74
74
  const inner = descriptor.value;
75
75
  Object.defineProperty(LazyReadableBase.prototype, member, {
76
76
  ...descriptor,
77
- /** @this {any} @param {...any} args */
77
+ /** @this {import("stream").Readable} @param {...unknown} args */
78
78
  value: function (...args) {
79
79
  materialise(this);
80
80
  return inner.apply(this, args);
@@ -86,13 +86,13 @@ for (const member of [
86
86
  Object.defineProperty(LazyReadableBase.prototype, member, {
87
87
  ...descriptor,
88
88
  get: innerGet
89
- ? /** @this {any} */ function () {
89
+ ? /** @this {import("stream").Readable} */ function () {
90
90
  materialise(this);
91
91
  return innerGet.call(this);
92
92
  }
93
93
  : undefined,
94
94
  set: innerSet
95
- ? /** @this {any} @param {any} value */ function (value) {
95
+ ? /** @this {import("stream").Readable} @param {unknown} value */ function (value) {
96
96
  materialise(this);
97
97
  innerSet.call(this, value);
98
98
  }
@@ -110,19 +110,21 @@ const nodeReadable = /** @type {PropertyDescriptor} */ (
110
110
  Object.defineProperty(LazyReadableBase.prototype, "readable", {
111
111
  configurable: true,
112
112
  enumerable: false,
113
+ // `this` is loose in both: node's state field, which its typings do not declare, and this
114
+ // project's own flag
113
115
  /** @this {any} */
114
116
  get: function () {
115
117
  return this._readableState === undefined
116
118
  ? this._readableFlag === true
117
- : /** @type {any} */ (nodeReadable.get).call(this);
119
+ : /** @type {() => boolean} */ (nodeReadable.get).call(this);
118
120
  },
119
- /** @this {any} @param {any} value */
121
+ /** @this {any} @param {unknown} value */
120
122
  set: function (value) {
121
123
  if (this._readableState === undefined) {
122
124
  this._readableFlag = !!value;
123
125
  return;
124
126
  }
125
- /** @type {any} */ (nodeReadable.set).call(this, value);
127
+ /** @type {(value: unknown) => void} */ (nodeReadable.set).call(this, value);
126
128
  }
127
129
  });
128
130
 
@@ -67,7 +67,7 @@ for (const member of [
67
67
  const inner = descriptor.value;
68
68
  Object.defineProperty(LazyWritableBase.prototype, member, {
69
69
  ...descriptor,
70
- /** @this {any} @param {...any} args */
70
+ /** @this {import("stream").Writable} @param {...unknown} args */
71
71
  value: function (...args) {
72
72
  materialiseWritable(this);
73
73
  return inner.apply(this, args);
@@ -79,13 +79,13 @@ for (const member of [
79
79
  Object.defineProperty(LazyWritableBase.prototype, member, {
80
80
  ...descriptor,
81
81
  get: innerGet
82
- ? /** @this {any} */ function () {
82
+ ? /** @this {import("stream").Writable} */ function () {
83
83
  materialiseWritable(this);
84
84
  return innerGet.call(this);
85
85
  }
86
86
  : undefined,
87
87
  set: innerSet
88
- ? /** @this {any} @param {any} value */ function (value) {
88
+ ? /** @this {import("stream").Writable} @param {unknown} value */ function (value) {
89
89
  materialiseWritable(this);
90
90
  innerSet.call(this, value);
91
91
  }