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.
@@ -19,12 +19,14 @@ limitations under the License.
19
19
 
20
20
  const acorn = require("acorn");
21
21
  const { stringify, withDefaultCharset, withUtf8Charset, contentTypeFor } = require("./utils.js");
22
- // H3App, DeclarativeResponse and _cfg all exist at runtime but are missing from the
23
- // declaration file the package ships, so the module is read through a loose alias
22
+ // H3App, DeclarativeResponse and _cfg exist at runtime but are missing from the .d.ts the
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 = [
@@ -43,31 +45,27 @@ const allowedResMethods = [
43
45
 
44
46
  const allowedIdentifiers = ["query", "params", ...allowedResMethods];
45
47
 
46
- /** What res.type(x) sets the content type to, which is a lookup on a literal. */
48
+ /** What res.type(x) sets the content type to. A lookup on a literal. */
47
49
  const typeValueOf = (type) => (type.indexOf("/") === -1 ? contentTypeFor(type) : type);
48
50
 
49
51
  // what one instruction of a declarative response can carry, since uWS writes its length as a u16
50
52
  const MAX_INSTRUCTION_LENGTH = 65535;
51
53
 
52
- // the three that write a body, of which only one may appear
53
- // The headers a conditional request is answered from. A compiled response cannot read the request,
54
- // so it cannot honour one, and a handler that sets one has to stay on the ordinary path.
54
+ // Headers used to answer a conditional request. A compiled response cannot read the request, so a
55
+ // handler that sets one has to stay on the ordinary path.
55
56
  const VALIDATOR_HEADERS = new Set(["etag", "last-modified"]);
56
57
 
57
- // The statuses whose message carries no content. 205 is here for node's reason rather than
58
- // express's: express strips the body for 204 and 304, and node answers a 205 with a lone
59
- // Content-Length of zero, so all three come out of the ordinary path with no body at all.
58
+ // Statuses that carry no body. Express strips it for 204 and 304, node answers a 205 with
59
+ // Content-Length: 0. All three come out of the ordinary path with no body.
60
60
  const BODILESS_STATUSES = new Set([204, 205, 304]);
61
61
 
62
+ // the three that write a body, only one of them may appear
62
63
  const bodyMethods = new Set(["send", "json", "end"]);
63
- // and the four that finish the response, after which nothing a handler does is observable
64
+ // the four that finish the response, nothing a handler does after them is observable
64
65
  const terminalMethods = new Set(["send", "json", "end", "sendStatus"]);
65
66
 
66
- // Every node type the walk in filterNodes knows how to step through. A type that is missing here
67
- // is not rejected for being dangerous, it is rejected because the walk would step over it without
68
- // saying so: a sequence expression hid its calls completely, and `res.append("x", "1"), res.send("k")`
69
- // compiled into a 200 with no body and no headers at all. Anything unfamiliar falls back to
70
- // ordinary routing, which is always correct if slower.
67
+ // Node types filterNodes can walk. A missing one is refused because the walk would skip it in
68
+ // silence: `res.append("x", "1"), res.send("k")` compiled to a 200 with no body and no headers.
71
69
  const understoodNodeTypes = new Set([
72
70
  "ArrowFunctionExpression",
73
71
  "FunctionDeclaration",
@@ -90,10 +88,11 @@ const understoodNodeTypes = new Set([
90
88
  ]);
91
89
 
92
90
  /**
93
- * Every node type in the tree, found by walking whatever is there rather than by following named
94
- * edges, so that the answer does not depend on the walk being complete.
91
+ * Every node type in the tree. Walks all the keys instead of named edges, so the answer does not
92
+ * depend on the walk being complete.
95
93
  *
96
- * @param {any} node
94
+ * @param {any} node an acorn node, or an array or a scalar under one: walked by key, so no shape
95
+ * is assumed
97
96
  * @param {Set<string>} types
98
97
  */
99
98
  function collectNodeTypes(node, types) {
@@ -114,10 +113,9 @@ function collectNodeTypes(node, types) {
114
113
  }
115
114
 
116
115
  /**
117
- * The key a property writes, when it is one this can read: a plain name or a literal, never
118
- * computed and never a getter or a spread.
116
+ * The key a property writes. Only a plain name or a literal, never computed, a getter or a spread.
119
117
  *
120
- * @param {any} property
118
+ * @param {import("acorn").AnyNode} property an acorn node, a Property when it is one this reads
121
119
  * @returns {string|null} null when the shape is not one of those
122
120
  */
123
121
  function literalKeyOf(property) {
@@ -131,15 +129,11 @@ function literalKeyOf(property) {
131
129
  }
132
130
 
133
131
  /**
134
- * The value a literal expression denotes, for the shapes whose value is known at registration
135
- * time. Anything else throws, which the catch around the whole compiler turns into ordinary
136
- * routing: a response written once cannot contain something only a request could produce.
137
- *
138
- * This replaced reading the value back out of the source text, which could not see through a
139
- * nested object or an array and needed a regular expression to re-quote the keys.
132
+ * The value of a literal expression, for the shapes known at registration time. Anything else
133
+ * throws, and the catch around the compiler turns it into ordinary routing.
140
134
  *
141
- * @param {any} node
142
- * @returns {any}
135
+ * @param {import("acorn").AnyNode} node
136
+ * @returns {unknown} whatever the literal denotes
143
137
  */
144
138
  function literalValue(node) {
145
139
  switch (node.type) {
@@ -192,533 +186,620 @@ function literalValue(node) {
192
186
  }
193
187
 
194
188
  // generates a declarative response from a callback
195
- // uWS allows creating such responses and they are extremely fast
196
- // since you don't even have to call into Node.js at all
197
- // declarative response will only be created if callback is 'simple enough'
198
- // simple enough means:
199
- // - doesnt call external functions
200
- // - doesnt create variables
201
- // - only uses req.query and req.params
202
- // basically, its only simple, static responses
203
- module.exports = function compileDeclarative(cb, app) {
204
- try {
205
- let code = cb.toString();
206
- // convert anonymous functions to named ones to make it valid code
207
- if (code.startsWith("function") || code.startsWith("async function")) {
208
- code = code.replace(/function *\(/, "function __cb(");
209
- }
210
-
211
- // Everything below runs inside the try above, and any shape this does not understand falls
212
- // out as `return false`, which is the fallback to ordinary routing. That is the design, and
213
- // it is why the tree is walked loosely: acorn types every shape JavaScript can take, and
214
- // enumerating them here would be a second, worse copy of that catch.
215
- //
216
- // The list below looks like the thing to widen, and it is not. Counted over the 1113
217
- // handlers in this repository's tests, demo and benchmark scenarios: 42.6% call something
218
- // that is not res, which no syntax admitted here can reach, and admitting `const` would
219
- // unlock 7 handlers, 0.6%, one conditional a further 0.1%. On the demo and the benchmark
220
- // alone, where the handlers look more like an application's, the share that calls something
221
- // rises to 59% and the two would unlock nothing at all. What keeps a route off this path is
222
- // that its answer is not knowable until the request arrives, and a variable is only another
223
- // way to spell an answer that already was.
224
- /** @type {any[]} */
225
- const tokens = [...acorn.tokenizer(code, { ecmaVersion: "latest" })];
226
-
227
- if (
228
- tokens.some((token) =>
229
- [
230
- "throw",
231
- "new",
232
- "await",
233
- "try",
234
- "catch",
235
- "finally",
236
- "if",
237
- "else",
238
- "switch",
239
- "case",
240
- "default",
241
- "for",
242
- "while",
243
- "do",
244
- "var",
245
- "let",
246
- "const"
247
- ].includes(token.value)
248
- )
249
- ) {
250
- return false;
251
- }
252
-
253
- /** @type {any[]} */
254
- const parsed = parser.parse(code, { ecmaVersion: "latest" }).body;
255
- let fn = parsed[0];
256
-
257
- if (fn.type === "ExpressionStatement") {
258
- fn = fn.expression;
259
- }
260
-
261
- // check if it is a function
262
- if (fn.type !== "FunctionDeclaration" && fn.type !== "ArrowFunctionExpression") {
263
- return false;
264
- }
265
-
266
- // before anything is read out of the tree, since reading it is only meaningful for the
267
- // shapes the walk can see through
268
- const nodeTypes = new Set();
269
- collectNodeTypes(fn, nodeTypes);
270
- for (const type of nodeTypes) {
271
- if (!understoodNodeTypes.has(type)) {
272
- return false;
273
- }
274
- }
275
-
276
- const args = fn.params.map((param) => param.name);
277
-
278
- if (args.length < 2) {
279
- // invalid function? doesn't have (req, res) args
280
- return false;
281
- }
282
-
283
- // `return res.send(...)` describes the same response as `res.send(...)` and is written far
284
- // more often, so it is worth reading. It only describes the same response when nothing
285
- // follows it: every call in the body is read, whether or not it can be reached, so a return
286
- // in the middle would compile statements that never run. A return inside a nested function
287
- // fails the same test, not being the last statement of this one.
288
- const returns = filterNodes(fn, (node) => node.type === "ReturnStatement");
289
- if (returns.length) {
290
- const statements = fn.body.type === "BlockStatement" ? fn.body.body : null;
291
- if (!statements || returns.length > 1 || returns[0] !== statements[statements.length - 1]) {
292
- return false;
189
+ /**
190
+ * The status and the headers the calls set, in the order they were first written. null when one
191
+ * of them is not a literal this can read.
192
+ *
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
196
+ * @returns {{statusCode: number, sendStatusUsed: boolean}|null}
197
+ */
198
+ function readStatusAndHeaders(callExprs, headers) {
199
+ let statusCode = 200;
200
+ // sendStatus and a bare send() both leave the body empty, but sendStatus sends the status
201
+ // message and send() sends nothing
202
+ let sendStatusUsed = false;
203
+ // get statusCode
204
+ for (const call of callExprs) {
205
+ if (call.obj.propertyName === "status") {
206
+ if (call.arguments[0].type !== "Literal") {
207
+ return null;
293
208
  }
209
+ statusCode = call.arguments[0].value;
294
210
  }
211
+ }
295
212
 
296
- const [req, res] = args;
297
- let queryName, paramsName;
298
- const queries = [],
299
- params = [];
300
-
301
- if (fn.params[0].type === "ObjectPattern") {
302
- const query = fn.params[0].properties.find((prop) => prop.key.name === "query");
303
- const param = fn.params[0].properties.find((prop) => prop.key.name === "params");
304
-
305
- if (query?.value?.type === "Identifier") {
306
- queryName = query.value.name;
307
- } else if (query?.value?.type === "ObjectPattern") {
308
- for (const prop of query.value.properties) {
309
- if (prop.value.type !== "Identifier") {
310
- return false;
311
- }
312
- queries.push(prop.value.name);
213
+ // get headers
214
+ for (const call of callExprs) {
215
+ const isType = call.obj.propertyName === "type" || call.obj.propertyName === "contentType";
216
+ if (
217
+ call.obj.propertyName === "header" ||
218
+ call.obj.propertyName === "setHeader" ||
219
+ call.obj.propertyName === "set" ||
220
+ isType
221
+ ) {
222
+ // type() is set("content-type", ...) after a media type lookup. set() also takes a
223
+ // whole object, one pair per set(). setHeader is node's and takes only strings.
224
+ let pairs;
225
+ if (isType) {
226
+ if (call.arguments[0].type !== "Literal") {
227
+ return null;
313
228
  }
314
- } else {
315
- return false;
316
- }
317
-
318
- if (param?.value?.type === "Identifier") {
319
- paramsName = param.value.name;
320
- } else if (param?.value?.type === "ObjectPattern") {
321
- for (const prop of param.value.properties) {
322
- if (prop.value.type !== "Identifier") {
323
- return false;
229
+ pairs = [["content-type", typeValueOf(String(call.arguments[0].value))]];
230
+ } else if (call.arguments.length === 1 && call.obj.propertyName !== "setHeader") {
231
+ if (call.arguments[0].type !== "ObjectExpression") {
232
+ return null;
233
+ }
234
+ pairs = [];
235
+ for (const property of call.arguments[0].properties) {
236
+ const key = literalKeyOf(property);
237
+ if (key === null || property.value.type !== "Literal") {
238
+ return null;
324
239
  }
325
- params.push(prop.value.name);
240
+ pairs.push([key, String(property.value.value)]);
326
241
  }
327
242
  } else {
328
- return false;
329
- }
330
- }
331
-
332
- // check if it calls any other function other than the one in `res`
333
- const callExprs = filterNodes(fn, (node) => node.type === "CallExpression");
334
- const resCalls = [];
335
- for (const expr of callExprs) {
336
- let calleeName, propertyName;
337
-
338
- // get propertyName
339
- if (expr.type === "MemberExpression") {
340
- propertyName = expr.property.name;
341
- } else if (expr.type === "CallExpression") {
342
- propertyName = expr.callee?.property?.name ?? expr.callee?.name;
243
+ if (call.arguments[0].type !== "Literal" || call.arguments[1]?.type !== "Literal") {
244
+ return null;
245
+ }
246
+ // String() here: a numeric literal would reach uWS writeHeader as a number,
247
+ // and uWS refuses anything that is not a string
248
+ pairs = [[call.arguments[0].value, String(call.arguments[1].value)]];
343
249
  }
344
250
 
345
- // get calleeName
346
- switch (expr.callee.type) {
347
- case "Identifier":
348
- calleeName = expr.callee.name;
349
- break;
350
- case "MemberExpression":
351
- if (expr.callee.object.type === "Identifier") {
352
- calleeName = expr.callee.object.name;
353
- } else if (expr.callee.object.type === "CallExpression") {
354
- // function call chaining
355
- let callee = expr.callee;
356
- while (callee.object.callee) {
357
- callee = callee.object.callee;
358
- }
359
- if (callee.object.type !== "Identifier") {
360
- return false;
251
+ for (let [header, value] of pairs) {
252
+ const name = String(header).toLowerCase();
253
+ // res.set adds a charset to a content-type, res.setHeader does not: setHeader
254
+ // is node's and node does not know what a media type is
255
+ if (call.obj.propertyName !== "setHeader" && name === "content-type") {
256
+ value = withDefaultCharset(value);
257
+ }
258
+ const index = headers.findIndex((entry) => String(entry[0]).toLowerCase() === name);
259
+ if (index === -1) {
260
+ headers.push([header, value]);
261
+ } else {
262
+ // in place, so the header keeps the position it was first given
263
+ headers[index][1] = value;
264
+ // set replaces the header, so values appended after it go too. Replacing
265
+ // only the first left the response carrying both.
266
+ for (let i = headers.length - 1; i > index; i--) {
267
+ if (String(headers[i][0]).toLowerCase() === name) {
268
+ headers.splice(i, 1);
361
269
  }
362
- calleeName = callee.object.name;
363
270
  }
364
- break;
365
- default:
366
- return false;
271
+ }
367
272
  }
368
- // check if calleeName is res
369
- if (calleeName !== res) {
370
- return false;
273
+ } else if (call.obj.propertyName === "append") {
274
+ if (call.arguments[0].type !== "Literal" || call.arguments[1].type !== "Literal") {
275
+ return null;
371
276
  }
372
-
373
- const obj = { calleeName, propertyName };
374
- expr.obj = obj;
375
- resCalls.push(obj);
376
- }
377
-
378
- // check if res property being called are
379
- // - set, header, setHeader
380
- // - status
381
- // - send
382
- // - end
383
- for (const call of resCalls) {
384
- if (!allowedResMethods.includes(call.propertyName)) {
385
- return false;
277
+ headers.push([call.arguments[0].value, String(call.arguments[1].value)]);
278
+ } else if (call.obj.propertyName === "sendStatus") {
279
+ if (call.arguments[0].type !== "Literal") {
280
+ return null;
386
281
  }
282
+ statusCode = call.arguments[0].value;
283
+ sendStatusUsed = true;
387
284
  }
285
+ }
286
+ return { statusCode, sendStatusUsed };
287
+ }
388
288
 
389
- // In the order the calls run, which for a chain is not the order the tree is walked in:
390
- // res.status(201) is a node inside res.status(201).send("k"), so the walk reaches the outer
391
- // call first and read res.status(201).status(202) backwards. Ending position orders a chain
392
- // and separate statements alike.
393
- callExprs.sort((a, b) => a.end - b.end);
394
-
395
- // Nothing after the call that writes the body has any effect, since by then the response
396
- // has gone out: res.sendStatus(404) followed by res.set("x-a", "1") sends no x-a on
397
- // Express, and res.send("k") followed by res.status(201) is still a 200. Two calls that
398
- // each write a body is a mistake rather than a shape worth compiling, and falls back.
399
- const terminalIndex = callExprs.findIndex((call) => terminalMethods.has(call.obj.propertyName));
400
- if (terminalIndex !== -1) {
401
- for (let i = terminalIndex + 1; i < callExprs.length; i++) {
402
- if (terminalMethods.has(callExprs[i].obj.propertyName)) {
403
- return false;
404
- }
289
+ /**
290
+ * The body parts the calls write, pushed into `body`, with the content-type decisions they imply
291
+ * pushed into `headers`. null when one of the calls writes something this cannot read.
292
+ *
293
+ * @param {any[]} callExprs the res calls, in run order, as readStatusAndHeaders takes them
294
+ * @param {[string, string][]} headers the headers read so far, written to
295
+ * @param {any[]} body the body parts, written to; loose because a literal's value is kept as it is
296
+ * @param {Application} app the application, for the json settings
297
+ * @param {string[]} queries names bound by a destructured req.query
298
+ * @param {string[]} params names bound by a destructured req.params
299
+ * @returns {{sendUsed: boolean, bodyFromSend: boolean}|null}
300
+ */
301
+ function readBody(callExprs, headers, body, app, queries, params) {
302
+ // get body
303
+ let sendUsed = false;
304
+ // only send() gets an ETag. end() is node's and never computes one, and the ordinary path
305
+ // does the same.
306
+ let bodyFromSend = false;
307
+ for (const call of callExprs) {
308
+ if (bodyMethods.has(call.obj.propertyName)) {
309
+ if (sendUsed) {
310
+ return null;
405
311
  }
406
- callExprs.length = terminalIndex + 1;
407
- }
408
-
409
- // check if all identifiers are allowed
410
- const identifiers = filterNodes(fn, (node) => node.type === "Identifier")
411
- .slice(args.length)
412
- .map((id) => id.name);
413
- if (identifiers[identifiers.length - 1] === "__cb") {
414
- identifiers.pop();
415
- }
416
- if (
417
- !identifiers.every(
418
- (id, i) =>
419
- allowedIdentifiers.includes(id) ||
420
- id === req ||
421
- id === res ||
422
- (identifiers[i - 2] === req && identifiers[i - 1] === "params") ||
423
- (identifiers[i - 2] === req && identifiers[i - 1] === "query") ||
424
- id === queryName ||
425
- id === paramsName ||
426
- queries.includes(id) ||
427
- params.includes(id)
428
- )
429
- ) {
430
- return false;
431
- }
432
-
433
- let statusCode = 200;
434
- // sendStatus and a bare send() both leave the body empty, and they mean different things:
435
- // one sends the status message, the other sends nothing
436
- let sendStatusUsed = false;
437
- const headers = [];
438
- const body = [];
312
+ // send() with no argument gets no content-type, same as Express and as the ordinary
313
+ // path. It was given one here anyway, so the two paths disagreed on `res.send()`.
314
+ if (call.obj.propertyName !== "end") {
315
+ bodyFromSend = true;
316
+ }
317
+ // one argument at most: res.end(data, encoding) and res.end(data, cb) are shapes a
318
+ // compiled response cannot stand for, and stood for them as if the extra were not there
319
+ if (call.arguments.length > 1) {
320
+ return null;
321
+ }
322
+ const arg = call.arguments[0];
439
323
 
440
- // get statusCode
441
- for (const call of callExprs) {
442
- if (call.obj.propertyName === "status") {
443
- if (call.arguments[0].type !== "Literal") {
444
- return false;
324
+ if (call.obj.propertyName === "json") {
325
+ // res.json() with no argument sends no body and no length, a shape left to the
326
+ // ordinary path
327
+ if (!arg) {
328
+ return null;
445
329
  }
446
- statusCode = call.arguments[0].value;
330
+ // a replacer runs per response on the ordinary path, so a body computed once
331
+ // could not honour it
332
+ const replacer = app.get("json replacer");
333
+ if (typeof replacer !== "undefined" && typeof replacer !== "string") {
334
+ return null;
335
+ }
336
+ // json sets a type only when none was chosen, then hands a string to send,
337
+ // which adds the charset to whatever type is there
338
+ const existing = headers.find((header) => header[0].toLowerCase() === "content-type");
339
+ if (existing) {
340
+ existing[1] = withUtf8Charset(String(existing[1]));
341
+ } else {
342
+ headers.push(["content-type", "application/json; charset=utf-8"]);
343
+ }
344
+ body.push({
345
+ type: "text",
346
+ value: stringify(literalValue(arg), replacer, app.get("json spaces"), app.get("json escape"))
347
+ });
348
+ sendUsed = true;
349
+ continue;
447
350
  }
448
- }
449
351
 
450
- // get headers
451
- for (const call of callExprs) {
452
- const isType = call.obj.propertyName === "type" || call.obj.propertyName === "contentType";
453
- if (
454
- call.obj.propertyName === "header" ||
455
- call.obj.propertyName === "setHeader" ||
456
- call.obj.propertyName === "set" ||
457
- isType
458
- ) {
459
- // type() is set("content-type", ...) with the media type looked up first, and
460
- // set() takes a whole object as well, which is one set() per pair. setHeader is
461
- // node's and throws on anything but a string, so it is not offered the object.
462
- let pairs;
463
- if (isType) {
464
- if (call.arguments[0].type !== "Literal") {
465
- return false;
352
+ if (call.obj.propertyName === "send" && arg) {
353
+ // The body decides the content-type, so this runs before the body is read.
354
+ // Doing it after made res.set("content-type", "text/plain") + res.send({})
355
+ // answer application/json, where Express answers text/plain.
356
+ const isJsonBody =
357
+ arg.type === "ObjectExpression" || (arg.type === "Literal" && typeof arg.value === "boolean");
358
+ const isNullBody = arg.type === "Literal" && arg.value === null;
359
+ const existing = headers.find((header) => header[0].toLowerCase() === "content-type");
360
+ if (!existing) {
361
+ if (isJsonBody) {
362
+ headers.push(["content-type", "application/json; charset=utf-8"]);
363
+ } else if (!isNullBody) {
364
+ // send(null) sends an empty string and chooses no type, the same as
365
+ // the ordinary path
366
+ headers.push(["content-type", "text/html; charset=utf-8"]);
466
367
  }
467
- pairs = [["content-type", typeValueOf(String(call.arguments[0].value))]];
468
- } else if (call.arguments.length === 1 && call.obj.propertyName !== "setHeader") {
469
- if (call.arguments[0].type !== "ObjectExpression") {
470
- return false;
368
+ } else {
369
+ existing[1] = withUtf8Charset(String(existing[1]));
370
+ }
371
+ }
372
+ if (arg) {
373
+ if (arg.type === "Literal") {
374
+ if (typeof arg.value === "number") {
375
+ // status code
376
+ return null;
471
377
  }
472
- pairs = [];
473
- for (const property of call.arguments[0].properties) {
474
- const key = literalKeyOf(property);
475
- if (key === null || property.value.type !== "Literal") {
476
- return false;
378
+ // the content-type was decided above, from what this argument is
379
+ const val = arg.value === null ? "" : arg.value;
380
+ body.push({ type: "text", value: val });
381
+ } else if (arg.type === "TemplateLiteral") {
382
+ const exprs = [...arg.quasis, ...arg.expressions].sort((a, b) => a.start - b.start);
383
+ for (const expr of exprs) {
384
+ if (expr.type === "TemplateElement") {
385
+ body.push({ type: "text", value: expr.value.cooked });
386
+ } else if (expr.type === "MemberExpression") {
387
+ const obj = expr.object;
388
+ let type;
389
+ if (obj.type === "MemberExpression") {
390
+ if (obj.property.type !== "Identifier") {
391
+ return null;
392
+ }
393
+ type = obj.property.name;
394
+ } else if (obj.type === "Identifier") {
395
+ type = obj.name;
396
+ } else {
397
+ return null;
398
+ }
399
+ if (type !== "params" && type !== "query") {
400
+ return null;
401
+ }
402
+ body.push({ type, value: expr.property.name });
403
+ } else if (expr.type === "Identifier") {
404
+ if (queries.includes(expr.name)) {
405
+ body.push({ type: "query", value: expr.name });
406
+ } else if (params.includes(expr.name)) {
407
+ body.push({ type: "params", value: expr.name });
408
+ } else {
409
+ return null;
410
+ }
411
+ } else {
412
+ return null;
477
413
  }
478
- pairs.push([key, String(property.value.value)]);
479
414
  }
480
- } else {
481
- if (call.arguments[0].type !== "Literal" || call.arguments[1]?.type !== "Literal") {
482
- return false;
415
+ } else if (arg.type === "MemberExpression") {
416
+ if (!arg.object.property) {
417
+ return null;
483
418
  }
484
- // String() at capture: a numeric literal would reach uWS's writeHeader as
485
- // itself, and uWS refuses anything that is not a string
486
- pairs = [[call.arguments[0].value, String(call.arguments[1].value)]];
487
- }
488
-
489
- for (let [header, value] of pairs) {
490
- const name = String(header).toLowerCase();
491
- // res.set charsets a content-type and res.setHeader does not, since the second
492
- // is node's and node does not know what a media type is
493
- if (call.obj.propertyName !== "setHeader" && name === "content-type") {
494
- value = withDefaultCharset(value);
419
+ if (
420
+ arg.object.property.type !== "Identifier" ||
421
+ (arg.object.property.name !== "query" && arg.object.property.name !== "params")
422
+ ) {
423
+ return null;
495
424
  }
496
- const index = headers.findIndex((entry) => String(entry[0]).toLowerCase() === name);
497
- if (index === -1) {
498
- headers.push([header, value]);
499
- } else {
500
- // in place, so the header keeps the position it was first given
501
- headers[index][1] = value;
502
- // set replaces the header outright, so any further value append left there
503
- // goes with it. Replacing only the first left the response carrying both.
504
- for (let i = headers.length - 1; i > index; i--) {
505
- if (String(headers[i][0]).toLowerCase() === name) {
506
- headers.splice(i, 1);
507
- }
425
+ body.push({ type: arg.object.property.name, value: arg.property.name });
426
+ } else if (arg.type === "BinaryExpression") {
427
+ const stuff = [];
428
+ /**
429
+ * Reads a chain of string concatenations right to left. Each side must be a literal or a
430
+ * param or query value, anything else makes the whole handler fall back.
431
+ *
432
+ * @param {any} node a BinaryExpression, read loosely: the literal on either
433
+ * side is kept as it is
434
+ * @returns {boolean}
435
+ */
436
+ function check(node) {
437
+ // only "+" concatenates, any other operator computes a value the parts
438
+ // cannot hold, so the handler falls back
439
+ if (node.operator !== "+") {
440
+ return false;
508
441
  }
442
+ if (node.right.type === "Literal") {
443
+ stuff.push({ type: "text", value: node.right.value });
444
+ } else if (node.right.type === "MemberExpression") {
445
+ stuff.push({ type: node.right.object.property.name, value: node.right.property.name });
446
+ } else return false;
447
+ if (node.left.type === "Literal") {
448
+ stuff.push({ type: "text", value: node.left.value });
449
+ } else if (node.left.type === "MemberExpression") {
450
+ stuff.push({ type: node.left.object.property.name, value: node.left.property.name });
451
+ } else if (node.left.type === "BinaryExpression") {
452
+ return check(node.left);
453
+ } else return false;
454
+
455
+ return true;
509
456
  }
510
- }
511
- } else if (call.obj.propertyName === "append") {
512
- if (call.arguments[0].type !== "Literal" || call.arguments[1].type !== "Literal") {
513
- return false;
514
- }
515
- headers.push([call.arguments[0].value, String(call.arguments[1].value)]);
516
- } else if (call.obj.propertyName === "sendStatus") {
517
- if (call.arguments[0].type !== "Literal") {
518
- return false;
519
- }
520
- statusCode = call.arguments[0].value;
521
- sendStatusUsed = true;
522
- }
523
- }
524
-
525
- // get body
526
- let sendUsed = false;
527
- // only send() earns an ETag. end() is node's, which never computes one, and the ordinary
528
- // path here follows that too, so the compiled path has to as well.
529
- let bodyFromSend = false;
530
- for (const call of callExprs) {
531
- if (bodyMethods.has(call.obj.propertyName)) {
532
- if (sendUsed) {
533
- return false;
534
- }
535
- // send() with nothing to send gets no content-type, the same as on the ordinary
536
- // path and the same as Express. It was being given one here regardless, so the
537
- // two paths disagreed about a route as simple as `res.send()`.
538
- if (call.obj.propertyName !== "end") {
539
- bodyFromSend = true;
540
- }
541
- const arg = call.arguments[0];
542
-
543
- if (call.obj.propertyName === "json") {
544
- // res.json() with nothing to serialise sends no body and no length, which is a
545
- // shape worth leaving to the ordinary path rather than reproducing here
546
- if (!arg) {
547
- return false;
457
+ if (!check(arg)) {
458
+ return null;
548
459
  }
549
- // a replacer runs per response on the ordinary path, so a body computed once
550
- // could not honour it
460
+ body.push(...stuff.reverse());
461
+ } else if (arg.type === "ObjectExpression") {
462
+ if (call.obj.propertyName === "end") {
463
+ return null;
464
+ }
465
+ // a replacer runs per response on the ordinary path, so a body computed
466
+ // once could not honour it
551
467
  const replacer = app.get("json replacer");
552
468
  if (typeof replacer !== "undefined" && typeof replacer !== "string") {
553
- return false;
554
- }
555
- // json reaches for a type only when none was chosen, then hands a string to
556
- // send, which is what charsets whatever type is there
557
- const existing = headers.find((header) => header[0].toLowerCase() === "content-type");
558
- if (existing) {
559
- existing[1] = withUtf8Charset(String(existing[1]));
560
- } else {
561
- headers.push(["content-type", "application/json; charset=utf-8"]);
469
+ return null;
562
470
  }
471
+
472
+ // the content-type was decided above, from what this argument is
563
473
  body.push({
564
474
  type: "text",
565
475
  value: stringify(literalValue(arg), replacer, app.get("json spaces"), app.get("json escape"))
566
476
  });
567
- sendUsed = true;
568
- continue;
477
+ } else {
478
+ return null;
569
479
  }
480
+ }
481
+ sendUsed = true;
482
+ }
483
+ }
484
+ return { sendUsed, bodyFromSend };
485
+ }
486
+ /**
487
+ * The handler's AST and its parameter names, when it is a shape this compiler can read at all.
488
+ * null for anything it cannot: a keyword it does not admit, a node type the walk cannot see
489
+ * through, too few parameters, or a return that is not the last statement.
490
+ *
491
+ * @param {Function} cb
492
+ * @returns {{fn: import("acorn").FunctionDeclaration|import("acorn").ArrowFunctionExpression, args: string[]}|null}
493
+ */
494
+ function readHandler(cb) {
495
+ let code = cb.toString();
496
+ // convert anonymous functions to named ones to make it valid code
497
+ if (code.startsWith("function") || code.startsWith("async function")) {
498
+ code = code.replace(/function *\(/, "function __cb(");
499
+ }
570
500
 
571
- if (call.obj.propertyName === "send" && arg) {
572
- // What the body is decides which content-type it would be given, so this comes
573
- // before the body is read rather than after. Setting one and then overwriting
574
- // it, which is what happened here, meant res.set("content-type", "text/plain")
575
- // followed by res.send({}) answered application/json where Express answers
576
- // text/plain: send only reaches for a type when none was chosen.
577
- const isJsonBody =
578
- arg.type === "ObjectExpression" || (arg.type === "Literal" && typeof arg.value === "boolean");
579
- const isNullBody = arg.type === "Literal" && arg.value === null;
580
- const existing = headers.find((header) => header[0].toLowerCase() === "content-type");
581
- if (!existing) {
582
- if (isJsonBody) {
583
- headers.push(["content-type", "application/json; charset=utf-8"]);
584
- } else if (!isNullBody) {
585
- // send(null) sends an empty string without choosing a type, the same
586
- // as it does on the ordinary path
587
- headers.push(["content-type", "text/html; charset=utf-8"]);
588
- }
589
- } else {
590
- existing[1] = withUtf8Charset(String(existing[1]));
591
- }
501
+ // Anything not understood returns false and falls back to ordinary routing. Widening the
502
+ // list below is not worth it: over the 1113 handlers in tests, demo and benchmark, 42.6%
503
+ // call something that is not res, `const` would unlock 7 (0.6%), a conditional 0.1% more.
504
+ /** @type {any[]} the tokens, loose because acorn's Token type leaves out value */
505
+ const tokens = [...acorn.tokenizer(code, { ecmaVersion: "latest" })];
506
+
507
+ if (
508
+ tokens.some((token) =>
509
+ [
510
+ "throw",
511
+ "new",
512
+ "await",
513
+ "try",
514
+ "catch",
515
+ "finally",
516
+ "if",
517
+ "else",
518
+ "switch",
519
+ "case",
520
+ "default",
521
+ "for",
522
+ "while",
523
+ "do",
524
+ "var",
525
+ "let",
526
+ "const"
527
+ ].includes(token.value)
528
+ )
529
+ ) {
530
+ return null;
531
+ }
532
+
533
+ /** @type {any[]} the statements, read loosely: what a parameter may be is checked by hand in readParamNames */
534
+ const parsed = parser.parse(code, { ecmaVersion: "latest" }).body;
535
+ let fn = parsed[0];
536
+
537
+ if (fn.type === "ExpressionStatement") {
538
+ fn = fn.expression;
539
+ }
540
+
541
+ // check if it is a function
542
+ if (fn.type !== "FunctionDeclaration" && fn.type !== "ArrowFunctionExpression") {
543
+ return null;
544
+ }
545
+
546
+ // before reading the tree, because reading is only valid for the shapes the walk can see
547
+ // through
548
+ const nodeTypes = new Set();
549
+ collectNodeTypes(fn, nodeTypes);
550
+ for (const type of nodeTypes) {
551
+ if (!understoodNodeTypes.has(type)) {
552
+ return null;
553
+ }
554
+ }
555
+
556
+ const args = fn.params.map((param) => param.name);
557
+
558
+ if (args.length < 2) {
559
+ // invalid function? doesn't have (req, res) args
560
+ return null;
561
+ }
562
+
563
+ // `return res.send(...)` is the same response as `res.send(...)`, but only as the last
564
+ // statement: every call is read, so a return in the middle would compile dead ones.
565
+ const returns = filterNodes(fn, (node) => node.type === "ReturnStatement");
566
+ if (returns.length) {
567
+ const statements = fn.body.type === "BlockStatement" ? fn.body.body : null;
568
+ if (!statements || returns.length > 1 || returns[0] !== statements[statements.length - 1]) {
569
+ return null;
570
+ }
571
+ }
572
+
573
+ return { fn, args };
574
+ }
575
+
576
+ /**
577
+ * What readParamNames found: the two parameter names, and what a destructured req bound.
578
+ * @typedef {object} ParamNames
579
+ * @property {string} req
580
+ * @property {string} res
581
+ * @property {string|undefined} queryName
582
+ * @property {string|undefined} paramsName
583
+ * @property {string[]} queries
584
+ * @property {string[]} params
585
+ */
586
+
587
+ /**
588
+ * The names a destructured `req` binds for query and params, so the body reader can tell one of
589
+ * them from an identifier it must refuse. null when the pattern is one this cannot read.
590
+ *
591
+ * @param {any} fn the handler's AST, read loosely: a destructured parameter is checked shape by
592
+ * shape, and anything else throws into the fallback
593
+ * @param {string[]} args its parameter names
594
+ * @returns {ParamNames|null}
595
+ */
596
+ function readParamNames(fn, args) {
597
+ const [req, res] = args;
598
+ let queryName, paramsName;
599
+ const queries = [],
600
+ params = [];
601
+
602
+ if (fn.params[0].type === "ObjectPattern") {
603
+ const query = fn.params[0].properties.find((prop) => prop.key.name === "query");
604
+ const param = fn.params[0].properties.find((prop) => prop.key.name === "params");
605
+
606
+ if (query?.value?.type === "Identifier") {
607
+ queryName = query.value.name;
608
+ } else if (query?.value?.type === "ObjectPattern") {
609
+ for (const prop of query.value.properties) {
610
+ if (prop.value.type !== "Identifier") {
611
+ return null;
592
612
  }
593
- if (arg) {
594
- if (arg.type === "Literal") {
595
- if (typeof arg.value === "number") {
596
- // status code
597
- return false;
598
- }
599
- // the content-type was decided above, from what this argument is
600
- const val = arg.value === null ? "" : arg.value;
601
- body.push({ type: "text", value: val });
602
- } else if (arg.type === "TemplateLiteral") {
603
- const exprs = [...arg.quasis, ...arg.expressions].sort((a, b) => a.start - b.start);
604
- for (const expr of exprs) {
605
- if (expr.type === "TemplateElement") {
606
- body.push({ type: "text", value: expr.value.cooked });
607
- } else if (expr.type === "MemberExpression") {
608
- const obj = expr.object;
609
- let type;
610
- if (obj.type === "MemberExpression") {
611
- if (obj.property.type !== "Identifier") {
612
- return false;
613
- }
614
- type = obj.property.name;
615
- } else if (obj.type === "Identifier") {
616
- type = obj.name;
617
- } else {
618
- return false;
619
- }
620
- if (type !== "params" && type !== "query") {
621
- return false;
622
- }
623
- body.push({ type, value: expr.property.name });
624
- } else if (expr.type === "Identifier") {
625
- if (queries.includes(expr.name)) {
626
- body.push({ type: "query", value: expr.name });
627
- } else if (params.includes(expr.name)) {
628
- body.push({ type: "params", value: expr.name });
629
- } else {
630
- return false;
631
- }
632
- } else {
633
- return false;
634
- }
635
- }
636
- } else if (arg.type === "MemberExpression") {
637
- if (!arg.object.property) {
638
- return false;
639
- }
640
- if (
641
- arg.object.property.type !== "Identifier" ||
642
- (arg.object.property.name !== "query" && arg.object.property.name !== "params")
643
- ) {
644
- return false;
645
- }
646
- body.push({ type: arg.object.property.name, value: arg.property.name });
647
- } else if (arg.type === "BinaryExpression") {
648
- const stuff = [];
649
- /**
650
- * Reads a chain of string concatenations right to left, collecting each side as either a
651
- * literal or a param or query value. Returns false for anything else, which is what makes
652
- * the whole handler fall back.
653
- *
654
- * @param {any} node a BinaryExpression
655
- * @returns {boolean}
656
- */
657
- function check(node) {
658
- // only "+" concatenates; any other operator computes a value the
659
- // parts cannot represent, so the handler falls back
660
- if (node.operator !== "+") {
661
- return false;
662
- }
663
- if (node.right.type === "Literal") {
664
- stuff.push({ type: "text", value: node.right.value });
665
- } else if (node.right.type === "MemberExpression") {
666
- stuff.push({ type: node.right.object.property.name, value: node.right.property.name });
667
- } else return false;
668
- if (node.left.type === "Literal") {
669
- stuff.push({ type: "text", value: node.left.value });
670
- } else if (node.left.type === "MemberExpression") {
671
- stuff.push({ type: node.left.object.property.name, value: node.left.property.name });
672
- } else if (node.left.type === "BinaryExpression") {
673
- return check(node.left);
674
- } else return false;
675
-
676
- return true;
677
- }
678
- if (!check(arg)) {
679
- return false;
680
- }
681
- body.push(...stuff.reverse());
682
- } else if (arg.type === "ObjectExpression") {
683
- if (call.obj.propertyName === "end") {
684
- return false;
685
- }
686
- // a replacer runs per response on the ordinary path, so a body computed
687
- // once could not honour it
688
- const replacer = app.get("json replacer");
689
- if (typeof replacer !== "undefined" && typeof replacer !== "string") {
690
- return false;
691
- }
613
+ queries.push(prop.value.name);
614
+ }
615
+ } else {
616
+ return null;
617
+ }
618
+
619
+ if (param?.value?.type === "Identifier") {
620
+ paramsName = param.value.name;
621
+ } else if (param?.value?.type === "ObjectPattern") {
622
+ for (const prop of param.value.properties) {
623
+ if (prop.value.type !== "Identifier") {
624
+ return null;
625
+ }
626
+ params.push(prop.value.name);
627
+ }
628
+ } else {
629
+ return null;
630
+ }
631
+ }
632
+ return { req, res, queryName, paramsName, queries, params };
633
+ }
692
634
 
693
- // the content-type was decided above, from what this argument is
694
- body.push({
695
- type: "text",
696
- value: stringify(
697
- literalValue(arg),
698
- replacer,
699
- app.get("json spaces"),
700
- app.get("json escape")
701
- )
702
- });
703
- } else {
704
- return false;
635
+ /**
636
+ * Every call the handler makes, in the order they run, cut after the one that writes the body.
637
+ * null when it calls anything but `res`, or a method a compiled response cannot stand for.
638
+ *
639
+ * @param {import("acorn").FunctionDeclaration|import("acorn").ArrowFunctionExpression} fn the handler's AST
640
+ * @param {string} res the name its second parameter was given
641
+ * @returns {any[]|null} the call nodes, each carrying what was read off its callee as `obj`; loose
642
+ * because the readers take their arguments as whatever literal they hold
643
+ */
644
+ function readResCalls(fn, res) {
645
+ // check if it calls any other function other than the one in `res`
646
+ const callExprs = filterNodes(fn, (node) => node.type === "CallExpression");
647
+ const resCalls = [];
648
+ for (const expr of callExprs) {
649
+ let calleeName, propertyName;
650
+
651
+ // get propertyName
652
+ if (expr.type === "MemberExpression") {
653
+ propertyName = expr.property.name;
654
+ } else if (expr.type === "CallExpression") {
655
+ propertyName = expr.callee?.property?.name ?? expr.callee?.name;
656
+ }
657
+
658
+ // get calleeName
659
+ switch (expr.callee.type) {
660
+ case "Identifier":
661
+ calleeName = expr.callee.name;
662
+ break;
663
+ case "MemberExpression":
664
+ if (expr.callee.object.type === "Identifier") {
665
+ calleeName = expr.callee.object.name;
666
+ } else if (expr.callee.object.type === "CallExpression") {
667
+ // function call chaining
668
+ let callee = expr.callee;
669
+ while (callee.object.callee) {
670
+ callee = callee.object.callee;
671
+ }
672
+ if (callee.object.type !== "Identifier") {
673
+ return null;
705
674
  }
675
+ calleeName = callee.object.name;
706
676
  }
707
- sendUsed = true;
677
+ break;
678
+ default:
679
+ return null;
680
+ }
681
+ // check if calleeName is res
682
+ if (calleeName !== res) {
683
+ return null;
684
+ }
685
+
686
+ const obj = { calleeName, propertyName };
687
+ expr.obj = obj;
688
+ resCalls.push(obj);
689
+ }
690
+
691
+ // check if res property being called are
692
+ // - set, header, setHeader
693
+ // - status
694
+ // - send
695
+ // - end
696
+ for (const call of resCalls) {
697
+ if (!allowedResMethods.includes(call.propertyName)) {
698
+ return null;
699
+ }
700
+ }
701
+
702
+ // Sorted in run order. In a chain the walk reaches the outer call first, so
703
+ // res.status(201).status(202) was read backwards. End position orders both cases.
704
+ callExprs.sort((a, b) => a.end - b.end);
705
+
706
+ // Nothing after the body call has any effect: on Express res.send("k") then
707
+ // res.status(201) is still a 200. Two calls that both write a body fall back.
708
+ const terminalIndex = callExprs.findIndex((call) => terminalMethods.has(call.obj.propertyName));
709
+ if (terminalIndex !== -1) {
710
+ for (let i = terminalIndex + 1; i < callExprs.length; i++) {
711
+ if (terminalMethods.has(callExprs[i].obj.propertyName)) {
712
+ return null;
708
713
  }
709
714
  }
715
+ callExprs.length = terminalIndex + 1;
716
+ }
717
+ return callExprs;
718
+ }
719
+
720
+ /**
721
+ * Whether every identifier in the handler is one a compiled response can stand for.
722
+ *
723
+ * @param {import("acorn").FunctionDeclaration|import("acorn").ArrowFunctionExpression} fn the handler's AST
724
+ * @param {string[]} args its parameter names
725
+ * @param {ParamNames} names what a destructured req bound, from readParamNames
726
+ * @returns {boolean}
727
+ */
728
+ function identifiersAllowed(fn, args, names) {
729
+ const { req, res, queryName, paramsName, queries, params } = names;
730
+ const identifiers = filterNodes(fn, (node) => node.type === "Identifier")
731
+ .slice(args.length)
732
+ .map((id) => id.name);
733
+ if (identifiers[identifiers.length - 1] === "__cb") {
734
+ identifiers.pop();
735
+ }
736
+ return identifiers.every(
737
+ (id, i) =>
738
+ allowedIdentifiers.includes(id) ||
739
+ id === req ||
740
+ id === res ||
741
+ (identifiers[i - 2] === req && identifiers[i - 1] === "params") ||
742
+ (identifiers[i - 2] === req && identifiers[i - 1] === "query") ||
743
+ id === queryName ||
744
+ id === paramsName ||
745
+ queries.includes(id) ||
746
+ params.includes(id)
747
+ );
748
+ }
749
+ // uWS allows creating such responses and they are extremely fast
750
+ // since you don't even have to call into Node.js at all
751
+ // declarative response will only be created if callback is 'simple enough'
752
+ // simple enough means:
753
+ // - doesnt call external functions
754
+ // - doesnt create variables
755
+ // - only uses req.query and req.params
756
+ // basically, its only simple, static responses
757
+ module.exports = function compileDeclarative(cb, app) {
758
+ try {
759
+ const handler = readHandler(cb);
760
+ if (handler === null) {
761
+ return false;
762
+ }
763
+ const { fn, args } = handler;
764
+
765
+ const names = readParamNames(fn, args);
766
+ if (names === null) {
767
+ return false;
768
+ }
769
+ const { res, queries, params } = names;
770
+
771
+ const callExprs = readResCalls(fn, res);
772
+ if (callExprs === null) {
773
+ return false;
774
+ }
775
+
776
+ if (!identifiersAllowed(fn, args, names)) {
777
+ return false;
778
+ }
779
+
780
+ const headers = [];
781
+ const body = [];
782
+
783
+ const status = readStatusAndHeaders(callExprs, headers);
784
+ if (status === null) {
785
+ return false;
786
+ }
787
+ const { statusCode, sendStatusUsed } = status;
788
+
789
+ const read = readBody(callExprs, headers, body, app, queries, params);
790
+ if (read === null) {
791
+ return false;
792
+ }
793
+ const { sendUsed, bodyFromSend } = read;
710
794
 
711
- // a handler that never sends is not a response: Express leaves the request waiting, so
712
- // compiling the empty shape would answer a bare 200 where the ordinary path answers
713
- // nothing at all. It has to fall back instead.
795
+ // a handler that never sends is not a response: Express leaves the request waiting, so this
796
+ // has to fall back instead of answering a bare 200
714
797
  if (!sendUsed && !sendStatusUsed) {
715
798
  return false;
716
799
  }
717
800
 
718
- // A status that carries no content. Compiled, the body went out with it, and a client
719
- // frames these as bodiless whatever the headers say, so those bytes were read as the start
720
- // of the next answer on the connection. The ordinary path already writes all three the way
721
- // express does.
801
+ // A status that carries no content. Compiled, the body went out anyway, and a client frames
802
+ // these as bodiless, so those bytes were read as the start of the next answer.
722
803
  if (BODILESS_STATUSES.has(statusCode) || statusCode < 200) {
723
804
  return false;
724
805
  }
@@ -729,22 +810,21 @@ module.exports = function compileDeclarative(cb, app) {
729
810
  const statusMessage = statuses.message[statusCode] ?? "unknown";
730
811
  decRes = decRes.writeStatus(`${statusCode} ${statusMessage}`);
731
812
  }
732
- // only sendStatus types its body: it goes through res.type("txt") on the ordinary path,
733
- // while status(n).end() sends no Content-Type at all, in Express and here alike
813
+ // only sendStatus types its body, through res.type("txt"). status(n).end() sends no
814
+ // Content-Type at all, in Express and here
734
815
  if (sendStatusUsed && !headers.some((header) => header[0].toLowerCase() === "content-type")) {
735
816
  decRes = decRes.writeHeader("content-type", "text/plain; charset=utf-8");
736
817
  }
737
818
 
738
819
  // the same two the ordinary path seeds every response with. Without them a route answered
739
- // different headers depending only on whether it happened to be compilable, which is worse
740
- // than either choice on its own, and a client had no idle timeout to go on.
820
+ // different headers only because it was compilable, and a client had no idle timeout.
741
821
  const advertise = app.get("connection headers") !== false;
742
822
  const connection = headers.find((header) => header[0].toLowerCase() === "connection");
743
823
  if (!connection && advertise) {
744
824
  decRes = decRes.writeHeader("connection", "keep-alive");
745
825
  }
746
- // not on a connection the handler is closing: Keep-Alive describes one that is staying
747
- // open, and the ordinary path leaves it out for the same reason
826
+ // not when the handler is closing: Keep-Alive describes a connection that stays open, and
827
+ // the ordinary path leaves it out for the same reason
748
828
  const closing = typeof connection?.[1] === "string" && connection[1].toLowerCase() === "close";
749
829
  if (advertise && !closing && !headers.some((header) => header[0].toLowerCase() === "keep-alive")) {
750
830
  decRes = decRes.writeHeader("keep-alive", "timeout=10");
@@ -755,42 +835,35 @@ module.exports = function compileDeclarative(cb, app) {
755
835
  if (name === "content-length") {
756
836
  return false;
757
837
  }
758
- // lowercased as the ordinary path stores every name, so the two paths answer the
759
- // same bytes whatever casing the handler wrote, see issue #7
838
+ // lowercased like the ordinary path stores them, so both paths answer the same bytes
839
+ // whatever casing the handler wrote, see issue #7
760
840
  decRes = decRes.writeHeader(name, header[1]);
761
841
  }
762
842
 
763
- // sendStatus sends the status message as its body. It has to join `body` here, before the
764
- // ETag is computed, and not at write time: computing the ETag over an empty body gave every
765
- // sendStatus response the same one, so a cache could not tell a 404 from a 500 and a
766
- // conditional request could be answered 304 with the wrong body entirely.
843
+ // sendStatus sends the status message as body, and it has to join `body` before the ETag:
844
+ // over an empty body every sendStatus response got the same ETag.
767
845
  if (sendStatusUsed && !body.length) {
768
846
  body.push({ type: "text", value: statuses.message[statusCode] || String(statusCode) });
769
847
  }
770
848
 
771
- // A response that would carry a validator is not compiled at all: µWS answers it without
772
- // reading the request, so it could never turn a conditional GET into the 304 the validator
773
- // invites. Dropping the ETag instead would leave the simplest routes of an application
774
- // without one, so `etag` false is how a route stays compiled.
849
+ // A response carrying a validator is not compiled: uWS answers without reading the request,
850
+ // so it could never turn a conditional GET into a 304. Use `etag` false to stay compiled.
775
851
  if (headers.some((header) => VALIDATOR_HEADERS.has(header[0].toLowerCase()))) {
776
852
  return false;
777
853
  }
778
- // an empty body gets no ETag, in Express and on the ordinary path here, so it has nothing
779
- // to lose by being compiled
854
+ // an empty body gets no ETag in Express either, so it loses nothing by being compiled
780
855
  if (body.length && (bodyFromSend || sendStatusUsed) && app.get("etag")) {
781
856
  return false;
782
857
  }
783
858
 
784
- // No Content-Length header here: uWS writes the framing itself, and a response carrying
785
- // both is invalid. Which framing it writes is decided at the end of this function.
859
+ // No Content-Length here: uWS writes the framing itself and a response with both is
860
+ // invalid. Which framing it writes is decided at the end of this function.
786
861
  if (app.get("x-powered-by")) {
787
862
  decRes = decRes.writeHeader("x-powered-by", "Fulmine");
788
863
  }
789
864
 
790
- // A body that is literal all the way through goes out as one end(), which is what makes
791
- // uWS frame it with a Content-Length, as Express does. A part interpolated from the
792
- // request has no length until the request arrives, so those stay a write each and uWS
793
- // chunks them.
865
+ // A fully literal body goes out as one end(), so uWS frames it with a Content-Length like
866
+ // Express. A part taken from the request has no length yet, so those are chunked writes.
794
867
  const literal = body.every((part) => part.type === "text")
795
868
  ? body.map((part) => String(part.value)).join("")
796
869
  : null;
@@ -815,14 +888,13 @@ module.exports = function compileDeclarative(cb, app) {
815
888
  };
816
889
 
817
890
  /**
818
- * Every node in the tree matching the predicate, in the order the named edges below are followed.
819
- * The edges are written out rather than discovered, which is why compileDeclarative first refuses
820
- * any node type that is not on the understood list: a shape this walk cannot see through would
821
- * otherwise be compiled as though it were empty.
891
+ * Every node matching the predicate, in the order of the named edges below. The edges are written
892
+ * by hand, which is why compileDeclarative first refuses any node type not on the understood list.
822
893
  *
823
- * @param {any} node
824
- * @param {(node: any) => boolean} fn
825
- * @returns {any[]}
894
+ * @param {any} node an acorn node, walked along the named edges below, so nothing is assumed
895
+ * about its shape
896
+ * @param {(node: import("acorn").AnyNode) => boolean} fn
897
+ * @returns {any[]} the matching nodes, as loose as the input
826
898
  */
827
899
  function filterNodes(node, fn) {
828
900
  const filtered = [];
@@ -887,9 +959,8 @@ function filterNodes(node, fn) {
887
959
  }
888
960
  }
889
961
 
890
- // singular, and not the list above: it is what a return statement returns, and what a unary
891
- // operator applies to. Without it `return res.send("x")` looked like a body with no calls in
892
- // it at all, which would have compiled into an empty response.
962
+ // singular, not the list above: what a return statement returns and what a unary operator
963
+ // applies to. Without it `return res.send("x")` looked like a body with no calls in it.
893
964
  if (node.argument) {
894
965
  filtered.push(...filterNodes(node.argument, fn));
895
966
  }