fulmine.js 5.19.2 → 5.19.3

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