fulmine.js 5.0.0-rc.1

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.
@@ -0,0 +1,768 @@
1
+ const acorn = require("acorn");
2
+ const { stringify, withDefaultCharset, withUtf8Charset } = require("./utils.js");
3
+ // H3App, DeclarativeResponse and _cfg all exist at runtime but are missing from the
4
+ // declaration file the package ships, so the module is read through a loose alias
5
+ const uWS = require("uWebSockets.js");
6
+ const uWSAny = /** @type {any} */ (uWS);
7
+ const statuses = require("statuses");
8
+
9
+ const parser = acorn.Parser;
10
+
11
+ const allowedResMethods = ["set", "header", "setHeader", "sendStatus", "status", "send", "json", "end", "append"];
12
+ const allowedIdentifiers = ["query", "params", ...allowedResMethods];
13
+ // the three that write a body, of which only one may appear
14
+ const bodyMethods = new Set(["send", "json", "end"]);
15
+ // and the four that finish the response, after which nothing a handler does is observable
16
+ const terminalMethods = new Set(["send", "json", "end", "sendStatus"]);
17
+
18
+ // Every node type the walk in filterNodes knows how to step through. A type that is missing here
19
+ // is not rejected for being dangerous, it is rejected because the walk would step over it without
20
+ // saying so: a sequence expression hid its calls completely, and `res.append("x", "1"), res.send("k")`
21
+ // compiled into a 200 with no body and no headers at all. Anything unfamiliar falls back to
22
+ // ordinary routing, which is always correct if slower.
23
+ const understoodNodeTypes = new Set([
24
+ "ArrowFunctionExpression",
25
+ "FunctionDeclaration",
26
+ "FunctionExpression",
27
+ "BlockStatement",
28
+ "ExpressionStatement",
29
+ "ReturnStatement",
30
+ "CallExpression",
31
+ "MemberExpression",
32
+ "Identifier",
33
+ "Literal",
34
+ "TemplateLiteral",
35
+ "TemplateElement",
36
+ "ObjectExpression",
37
+ "ArrayExpression",
38
+ "Property",
39
+ "BinaryExpression",
40
+ "UnaryExpression",
41
+ "ObjectPattern"
42
+ ]);
43
+
44
+ /**
45
+ * Every node type in the tree, found by walking whatever is there rather than by following named
46
+ * edges, so that the answer does not depend on the walk being complete.
47
+ *
48
+ * @param {any} node
49
+ * @param {Set<string>} types
50
+ */
51
+ function collectNodeTypes(node, types) {
52
+ if (!node || typeof node !== "object") {
53
+ return;
54
+ }
55
+ if (Array.isArray(node)) {
56
+ for (const child of node) collectNodeTypes(child, types);
57
+ return;
58
+ }
59
+ if (typeof node.type === "string") {
60
+ types.add(node.type);
61
+ }
62
+ for (const key in node) {
63
+ if (key === "type" || key === "start" || key === "end") continue;
64
+ collectNodeTypes(node[key], types);
65
+ }
66
+ }
67
+
68
+ /**
69
+ * The value a literal expression denotes, for the shapes whose value is known at registration
70
+ * time. Anything else throws, which the catch around the whole compiler turns into ordinary
71
+ * routing: a response written once cannot contain something only a request could produce.
72
+ *
73
+ * This replaced reading the value back out of the source text, which could not see through a
74
+ * nested object or an array and needed a regular expression to re-quote the keys.
75
+ *
76
+ * @param {any} node
77
+ * @returns {any}
78
+ */
79
+ function literalValue(node) {
80
+ switch (node.type) {
81
+ case "Literal":
82
+ // a regular expression and a bigint are literals that JSON cannot carry
83
+ if (node.regex || typeof node.value === "bigint") {
84
+ throw new Error("not serialisable");
85
+ }
86
+ return node.value;
87
+ case "ArrayExpression":
88
+ return node.elements.map((element) => {
89
+ // a hole, as in [1, , 2], and a spread, which needs something to spread
90
+ if (element === null || element.type === "SpreadElement") {
91
+ throw new Error("not a literal");
92
+ }
93
+ return literalValue(element);
94
+ });
95
+ case "ObjectExpression": {
96
+ const out = {};
97
+ for (const property of node.properties) {
98
+ if (property.type !== "Property" || property.computed || property.kind !== "init") {
99
+ throw new Error("not a literal");
100
+ }
101
+ const key =
102
+ property.key.type === "Identifier"
103
+ ? property.key.name
104
+ : property.key.type === "Literal"
105
+ ? String(property.key.value)
106
+ : null;
107
+ if (key === null) {
108
+ throw new Error("not a literal");
109
+ }
110
+ out[key] = literalValue(property.value);
111
+ }
112
+ return out;
113
+ }
114
+ case "UnaryExpression":
115
+ // -1 is a unary minus applied to a literal, not a literal
116
+ if (node.operator === "-" || node.operator === "+") {
117
+ const value = literalValue(node.argument);
118
+ if (typeof value !== "number") {
119
+ throw new Error("not a literal");
120
+ }
121
+ return node.operator === "-" ? -value : value;
122
+ }
123
+ throw new Error("not a literal");
124
+ default:
125
+ throw new Error("not a literal");
126
+ }
127
+ }
128
+
129
+ // generates a declarative response from a callback
130
+ // uWS allows creating such responses and they are extremely fast
131
+ // since you don't even have to call into Node.js at all
132
+ // declarative response will only be created if callback is 'simple enough'
133
+ // simple enough means:
134
+ // - doesnt call external functions
135
+ // - doesnt create variables
136
+ // - only uses req.query and req.params
137
+ // basically, its only simple, static responses
138
+ module.exports = function compileDeclarative(cb, app) {
139
+ try {
140
+ let code = cb.toString();
141
+ // convert anonymous functions to named ones to make it valid code
142
+ if (code.startsWith("function") || code.startsWith("async function")) {
143
+ code = code.replace(/function *\(/, "function __cb(");
144
+ }
145
+
146
+ // Everything below runs inside the try above, and any shape this does not understand falls
147
+ // out as `return false`, which is the fallback to ordinary routing. That is the design, and
148
+ // it is why the tree is walked loosely: acorn types every shape JavaScript can take, and
149
+ // enumerating them here would be a second, worse copy of that catch.
150
+ /** @type {any[]} */
151
+ const tokens = [...acorn.tokenizer(code, { ecmaVersion: "latest" })];
152
+
153
+ if (
154
+ tokens.some((token) =>
155
+ [
156
+ "throw",
157
+ "new",
158
+ "await",
159
+ "try",
160
+ "catch",
161
+ "finally",
162
+ "if",
163
+ "else",
164
+ "switch",
165
+ "case",
166
+ "default",
167
+ "for",
168
+ "while",
169
+ "do",
170
+ "var",
171
+ "let",
172
+ "const"
173
+ ].includes(token.value)
174
+ )
175
+ ) {
176
+ return false;
177
+ }
178
+
179
+ /** @type {any[]} */
180
+ const parsed = parser.parse(code, { ecmaVersion: "latest" }).body;
181
+ let fn = parsed[0];
182
+
183
+ if (fn.type === "ExpressionStatement") {
184
+ fn = fn.expression;
185
+ }
186
+
187
+ // check if it is a function
188
+ if (fn.type !== "FunctionDeclaration" && fn.type !== "ArrowFunctionExpression") {
189
+ return false;
190
+ }
191
+
192
+ // before anything is read out of the tree, since reading it is only meaningful for the
193
+ // shapes the walk can see through
194
+ const nodeTypes = new Set();
195
+ collectNodeTypes(fn, nodeTypes);
196
+ for (const type of nodeTypes) {
197
+ if (!understoodNodeTypes.has(type)) {
198
+ return false;
199
+ }
200
+ }
201
+
202
+ const args = fn.params.map((param) => param.name);
203
+
204
+ if (args.length < 2) {
205
+ // invalid function? doesn't have (req, res) args
206
+ return false;
207
+ }
208
+
209
+ // `return res.send(...)` describes the same response as `res.send(...)` and is written far
210
+ // more often, so it is worth reading. It only describes the same response when nothing
211
+ // follows it: every call in the body is read, whether or not it can be reached, so a return
212
+ // in the middle would compile statements that never run. A return inside a nested function
213
+ // fails the same test, not being the last statement of this one.
214
+ const returns = filterNodes(fn, (node) => node.type === "ReturnStatement");
215
+ if (returns.length) {
216
+ const statements = fn.body.type === "BlockStatement" ? fn.body.body : null;
217
+ if (!statements || returns.length > 1 || returns[0] !== statements[statements.length - 1]) {
218
+ return false;
219
+ }
220
+ }
221
+
222
+ const [req, res] = args;
223
+ let queryName,
224
+ paramsName,
225
+ queries = [],
226
+ params = [];
227
+
228
+ if (fn.params[0].type === "ObjectPattern") {
229
+ const query = fn.params[0].properties.find((prop) => prop.key.name === "query");
230
+ const param = fn.params[0].properties.find((prop) => prop.key.name === "params");
231
+
232
+ if (query?.value?.type === "Identifier") {
233
+ queryName = query.value.name;
234
+ } else if (query?.value?.type === "ObjectPattern") {
235
+ for (const prop of query.value.properties) {
236
+ if (prop.value.type !== "Identifier") {
237
+ return false;
238
+ }
239
+ queries.push(prop.value.name);
240
+ }
241
+ } else {
242
+ return false;
243
+ }
244
+
245
+ if (param?.value?.type === "Identifier") {
246
+ paramsName = param.value.name;
247
+ } else if (param?.value?.type === "ObjectPattern") {
248
+ for (const prop of param.value.properties) {
249
+ if (prop.value.type !== "Identifier") {
250
+ return false;
251
+ }
252
+ params.push(prop.value.name);
253
+ }
254
+ } else {
255
+ return false;
256
+ }
257
+ }
258
+
259
+ // check if it calls any other function other than the one in `res`
260
+ const callExprs = filterNodes(fn, (node) => node.type === "CallExpression");
261
+ const resCalls = [];
262
+ for (const expr of callExprs) {
263
+ let calleeName, propertyName;
264
+
265
+ // get propertyName
266
+ if (expr.type === "MemberExpression") {
267
+ propertyName = expr.property.name;
268
+ } else if (expr.type === "CallExpression") {
269
+ propertyName = expr.callee?.property?.name ?? expr.callee?.name;
270
+ }
271
+
272
+ // get calleeName
273
+ switch (expr.callee.type) {
274
+ case "Identifier":
275
+ calleeName = expr.callee.name;
276
+ break;
277
+ case "MemberExpression":
278
+ if (expr.callee.object.type === "Identifier") {
279
+ calleeName = expr.callee.object.name;
280
+ } else if (expr.callee.object.type === "CallExpression") {
281
+ // function call chaining
282
+ let callee = expr.callee;
283
+ while (callee.object.callee) {
284
+ callee = callee.object.callee;
285
+ }
286
+ if (callee.object.type !== "Identifier") {
287
+ return false;
288
+ }
289
+ calleeName = callee.object.name;
290
+ }
291
+ break;
292
+ default:
293
+ return false;
294
+ }
295
+ // check if calleeName is res
296
+ if (calleeName !== res) {
297
+ return false;
298
+ }
299
+
300
+ const obj = { calleeName, propertyName };
301
+ expr.obj = obj;
302
+ resCalls.push(obj);
303
+ }
304
+
305
+ // check if res property being called are
306
+ // - set, header, setHeader
307
+ // - status
308
+ // - send
309
+ // - end
310
+ for (const call of resCalls) {
311
+ if (!allowedResMethods.includes(call.propertyName)) {
312
+ return false;
313
+ }
314
+ }
315
+
316
+ // In the order the calls run, which for a chain is not the order the tree is walked in:
317
+ // res.status(201) is a node inside res.status(201).send("k"), so the walk reaches the outer
318
+ // call first and read res.status(201).status(202) backwards. Ending position orders a chain
319
+ // and separate statements alike.
320
+ callExprs.sort((a, b) => a.end - b.end);
321
+
322
+ // Nothing after the call that writes the body has any effect, since by then the response
323
+ // has gone out: res.sendStatus(404) followed by res.set("x-a", "1") sends no x-a on
324
+ // Express, and res.send("k") followed by res.status(201) is still a 200. Two calls that
325
+ // each write a body is a mistake rather than a shape worth compiling, and falls back.
326
+ const terminalIndex = callExprs.findIndex((call) => terminalMethods.has(call.obj.propertyName));
327
+ if (terminalIndex !== -1) {
328
+ for (let i = terminalIndex + 1; i < callExprs.length; i++) {
329
+ if (terminalMethods.has(callExprs[i].obj.propertyName)) {
330
+ return false;
331
+ }
332
+ }
333
+ callExprs.length = terminalIndex + 1;
334
+ }
335
+
336
+ // check if all identifiers are allowed
337
+ const identifiers = filterNodes(fn, (node) => node.type === "Identifier")
338
+ .slice(args.length)
339
+ .map((id) => id.name);
340
+ if (identifiers[identifiers.length - 1] === "__cb") {
341
+ identifiers.pop();
342
+ }
343
+ if (
344
+ !identifiers.every(
345
+ (id, i) =>
346
+ allowedIdentifiers.includes(id) ||
347
+ id === req ||
348
+ id === res ||
349
+ (identifiers[i - 2] === req && identifiers[i - 1] === "params") ||
350
+ (identifiers[i - 2] === req && identifiers[i - 1] === "query") ||
351
+ id === queryName ||
352
+ id === paramsName ||
353
+ queries.includes(id) ||
354
+ params.includes(id)
355
+ )
356
+ ) {
357
+ return false;
358
+ }
359
+
360
+ let statusCode = 200;
361
+ // sendStatus and a bare send() both leave the body empty, and they mean different things:
362
+ // one sends the status message, the other sends nothing
363
+ let sendStatusUsed = false;
364
+ const headers = [];
365
+ const body = [];
366
+
367
+ // get statusCode
368
+ for (const call of callExprs) {
369
+ if (call.obj.propertyName === "status") {
370
+ if (call.arguments[0].type !== "Literal") {
371
+ return false;
372
+ }
373
+ statusCode = call.arguments[0].value;
374
+ }
375
+ }
376
+
377
+ // get headers
378
+ for (const call of callExprs) {
379
+ if (
380
+ call.obj.propertyName === "header" ||
381
+ call.obj.propertyName === "setHeader" ||
382
+ call.obj.propertyName === "set"
383
+ ) {
384
+ if (call.arguments[0].type !== "Literal" || call.arguments[1].type !== "Literal") {
385
+ return false;
386
+ }
387
+ let [header, value] = [call.arguments[0].value, call.arguments[1].value];
388
+ const name = String(header).toLowerCase();
389
+ // res.set charsets a content-type and res.setHeader does not, since the second is
390
+ // node's and node does not know what a media type is
391
+ if (call.obj.propertyName !== "setHeader" && name === "content-type") {
392
+ value = withDefaultCharset(String(value));
393
+ }
394
+ const index = headers.findIndex((entry) => String(entry[0]).toLowerCase() === name);
395
+ if (index === -1) {
396
+ headers.push([header, value]);
397
+ } else {
398
+ // in place, so the header keeps the position it was first given
399
+ headers[index][1] = value;
400
+ // set replaces the header outright, so any further value append left there
401
+ // goes with it. Replacing only the first left the response carrying both.
402
+ for (let i = headers.length - 1; i > index; i--) {
403
+ if (String(headers[i][0]).toLowerCase() === name) {
404
+ headers.splice(i, 1);
405
+ }
406
+ }
407
+ }
408
+ } else if (call.obj.propertyName === "append") {
409
+ if (call.arguments[0].type !== "Literal" || call.arguments[1].type !== "Literal") {
410
+ return false;
411
+ }
412
+ headers.push([call.arguments[0].value, call.arguments[1].value]);
413
+ } else if (call.obj.propertyName === "sendStatus") {
414
+ if (call.arguments[0].type !== "Literal") {
415
+ return false;
416
+ }
417
+ statusCode = call.arguments[0].value;
418
+ sendStatusUsed = true;
419
+ }
420
+ }
421
+
422
+ // get body
423
+ let sendUsed = false;
424
+ // only send() earns an ETag. end() is node's, which never computes one, and the ordinary
425
+ // path here follows that too, so the compiled path has to as well.
426
+ let bodyFromSend = false;
427
+ for (const call of callExprs) {
428
+ if (bodyMethods.has(call.obj.propertyName)) {
429
+ if (sendUsed) {
430
+ return false;
431
+ }
432
+ // send() with nothing to send gets no content-type, the same as on the ordinary
433
+ // path and the same as Express. It was being given one here regardless, so the
434
+ // two paths disagreed about a route as simple as `res.send()`.
435
+ if (call.obj.propertyName !== "end") {
436
+ bodyFromSend = true;
437
+ }
438
+ const arg = call.arguments[0];
439
+
440
+ if (call.obj.propertyName === "json") {
441
+ // res.json() with nothing to serialise sends no body and no length, which is a
442
+ // shape worth leaving to the ordinary path rather than reproducing here
443
+ if (!arg) {
444
+ return false;
445
+ }
446
+ // a replacer runs per response on the ordinary path, so a body computed once
447
+ // could not honour it
448
+ const replacer = app.get("json replacer");
449
+ if (typeof replacer !== "undefined" && typeof replacer !== "string") {
450
+ return false;
451
+ }
452
+ // json reaches for a type only when none was chosen, then hands a string to
453
+ // send, which is what charsets whatever type is there
454
+ const existing = headers.find((header) => header[0].toLowerCase() === "content-type");
455
+ if (existing) {
456
+ existing[1] = withUtf8Charset(String(existing[1]));
457
+ } else {
458
+ headers.push(["content-type", "application/json; charset=utf-8"]);
459
+ }
460
+ body.push({
461
+ type: "text",
462
+ value: stringify(literalValue(arg), replacer, app.get("json spaces"), app.get("json escape"))
463
+ });
464
+ sendUsed = true;
465
+ continue;
466
+ }
467
+
468
+ if (call.obj.propertyName === "send" && arg) {
469
+ // What the body is decides which content-type it would be given, so this comes
470
+ // before the body is read rather than after. Setting one and then overwriting
471
+ // it, which is what happened here, meant res.set("content-type", "text/plain")
472
+ // followed by res.send({}) answered application/json where Express answers
473
+ // text/plain: send only reaches for a type when none was chosen.
474
+ const isJsonBody =
475
+ arg.type === "ObjectExpression" || (arg.type === "Literal" && typeof arg.value === "boolean");
476
+ const isNullBody = arg.type === "Literal" && arg.value === null;
477
+ const existing = headers.find((header) => header[0].toLowerCase() === "content-type");
478
+ if (!existing) {
479
+ if (isJsonBody) {
480
+ headers.push(["content-type", "application/json; charset=utf-8"]);
481
+ } else if (!isNullBody) {
482
+ // send(null) sends an empty string without choosing a type, the same
483
+ // as it does on the ordinary path
484
+ headers.push(["content-type", "text/html; charset=utf-8"]);
485
+ }
486
+ } else {
487
+ existing[1] = withUtf8Charset(String(existing[1]));
488
+ }
489
+ }
490
+ if (arg) {
491
+ if (arg.type === "Literal") {
492
+ if (typeof arg.value === "number") {
493
+ // status code
494
+ return false;
495
+ }
496
+ // the content-type was decided above, from what this argument is
497
+ const val = arg.value === null ? "" : arg.value;
498
+ body.push({ type: "text", value: val });
499
+ } else if (arg.type === "TemplateLiteral") {
500
+ const exprs = [...arg.quasis, ...arg.expressions].sort((a, b) => a.start - b.start);
501
+ for (const expr of exprs) {
502
+ if (expr.type === "TemplateElement") {
503
+ body.push({ type: "text", value: expr.value.cooked });
504
+ } else if (expr.type === "MemberExpression") {
505
+ const obj = expr.object;
506
+ let type;
507
+ if (obj.type === "MemberExpression") {
508
+ if (obj.property.type !== "Identifier") {
509
+ return false;
510
+ }
511
+ type = obj.property.name;
512
+ } else if (obj.type === "Identifier") {
513
+ type = obj.name;
514
+ } else {
515
+ return false;
516
+ }
517
+ if (type !== "params" && type !== "query") {
518
+ return false;
519
+ }
520
+ body.push({ type, value: expr.property.name });
521
+ } else if (expr.type === "Identifier") {
522
+ if (queries.includes(expr.name)) {
523
+ body.push({ type: "query", value: expr.name });
524
+ } else if (params.includes(expr.name)) {
525
+ body.push({ type: "params", value: expr.name });
526
+ } else {
527
+ return false;
528
+ }
529
+ } else {
530
+ return false;
531
+ }
532
+ }
533
+ } else if (arg.type === "MemberExpression") {
534
+ if (!arg.object.property) {
535
+ return false;
536
+ }
537
+ if (
538
+ arg.object.property.type !== "Identifier" ||
539
+ (arg.object.property.name !== "query" && arg.object.property.name !== "params")
540
+ ) {
541
+ return false;
542
+ }
543
+ body.push({ type: arg.object.property.name, value: arg.property.name });
544
+ } else if (arg.type === "BinaryExpression") {
545
+ const stuff = [];
546
+ /**
547
+ * Reads a chain of string concatenations right to left, collecting each side as either a
548
+ * literal or a param or query value. Returns false for anything else, which is what makes
549
+ * the whole handler fall back.
550
+ *
551
+ * @param {any} node a BinaryExpression
552
+ * @returns {boolean}
553
+ */
554
+ function check(node) {
555
+ if (node.right.type === "Literal") {
556
+ stuff.push({ type: "text", value: node.right.value });
557
+ } else if (node.right.type === "MemberExpression") {
558
+ stuff.push({ type: node.right.object.property.name, value: node.right.property.name });
559
+ } else return false;
560
+ if (node.left.type === "Literal") {
561
+ stuff.push({ type: "text", value: node.left.value });
562
+ } else if (node.left.type === "MemberExpression") {
563
+ stuff.push({ type: node.left.object.property.name, value: node.left.property.name });
564
+ } else if (node.left.type === "BinaryExpression") {
565
+ return check(node.left);
566
+ } else return false;
567
+
568
+ return true;
569
+ }
570
+ if (!check(arg)) {
571
+ return false;
572
+ }
573
+ body.push(...stuff.reverse());
574
+ } else if (arg.type === "ObjectExpression") {
575
+ if (call.obj.propertyName === "end") {
576
+ return false;
577
+ }
578
+ // a replacer runs per response on the ordinary path, so a body computed
579
+ // once could not honour it
580
+ const replacer = app.get("json replacer");
581
+ if (typeof replacer !== "undefined" && typeof replacer !== "string") {
582
+ return false;
583
+ }
584
+
585
+ // the content-type was decided above, from what this argument is
586
+ body.push({
587
+ type: "text",
588
+ value: stringify(
589
+ literalValue(arg),
590
+ replacer,
591
+ app.get("json spaces"),
592
+ app.get("json escape")
593
+ )
594
+ });
595
+ } else {
596
+ return false;
597
+ }
598
+ }
599
+ sendUsed = true;
600
+ }
601
+ }
602
+
603
+ let decRes = new uWSAny.DeclarativeResponse();
604
+
605
+ if (statusCode != 200) {
606
+ const statusMessage = statuses.message[statusCode] ?? "";
607
+ decRes = decRes.writeStatus(`${statusCode} ${statusMessage}`.trim());
608
+ if (!headers.some((header) => header[0].toLowerCase() === "content-type")) {
609
+ decRes = decRes.writeHeader("content-type", "text/plain; charset=utf-8");
610
+ }
611
+ }
612
+
613
+ // the same two the ordinary path seeds every response with. Without them a route answered
614
+ // different headers depending only on whether it happened to be compilable, which is worse
615
+ // than either choice on its own, and a client had no idle timeout to go on.
616
+ const connection = headers.find((header) => header[0].toLowerCase() === "connection");
617
+ if (!connection) {
618
+ decRes = decRes.writeHeader("connection", "keep-alive");
619
+ }
620
+ // not on a connection the handler is closing: Keep-Alive describes one that is staying
621
+ // open, and the ordinary path leaves it out for the same reason
622
+ const closing = typeof connection?.[1] === "string" && connection[1].toLowerCase() === "close";
623
+ if (!closing && !headers.some((header) => header[0].toLowerCase() === "keep-alive")) {
624
+ decRes = decRes.writeHeader("keep-alive", "timeout=10");
625
+ }
626
+
627
+ for (const header of headers) {
628
+ if (header[0].toLowerCase() === "content-length") {
629
+ return false;
630
+ }
631
+ decRes = decRes.writeHeader(header[0], header[1]);
632
+ }
633
+
634
+ // sendStatus sends the status message as its body. It has to join `body` here, before the
635
+ // ETag is computed, and not at write time: computing the ETag over an empty body gave every
636
+ // sendStatus response the same one, so a cache could not tell a 404 from a 500 and a
637
+ // conditional request could be answered 304 with the wrong body entirely.
638
+ if (sendStatusUsed && !body.length) {
639
+ body.push({ type: "text", value: statuses.message[statusCode] || String(statusCode) });
640
+ }
641
+
642
+ // an empty body gets no ETag, which is what Express does and what the ordinary path here
643
+ // already did
644
+ if (
645
+ body.length &&
646
+ (bodyFromSend || sendStatusUsed) &&
647
+ app.get("etag") &&
648
+ !headers.some((header) => header[0].toLowerCase() === "etag")
649
+ ) {
650
+ if (body.some((part) => part.type !== "text")) {
651
+ return false;
652
+ } else {
653
+ const etag = app.get("etag fn")(body.map((part) => part.value.toString()).join(""));
654
+ // an application's own etag function is allowed to decline, and a declarative
655
+ // response cannot answer with a header whose value is nothing
656
+ if (etag) {
657
+ decRes = decRes.writeHeader("ETag", etag);
658
+ }
659
+ }
660
+ }
661
+
662
+ // No Content-Length here, and it is not an oversight. uWS writes a DeclarativeResponse
663
+ // with Transfer-Encoding: chunked and adds that header itself, so setting Content-Length
664
+ // as well produces a response carrying both, which is invalid and which clients reject
665
+ // outright. Every declarative response is therefore chunked, where Express always sends
666
+ // a length. Changing it means changing uWS.
667
+ if (app.get("x-powered-by")) {
668
+ decRes = decRes.writeHeader("x-powered-by", "Fulmine");
669
+ }
670
+
671
+ for (const bodyPart of body) {
672
+ if (bodyPart.type === "text" && String(bodyPart.value).length) {
673
+ decRes = decRes.write(String(bodyPart.value));
674
+ } else if (bodyPart.type === "params") {
675
+ decRes = decRes.writeParameterValue(bodyPart.value);
676
+ } else if (bodyPart.type === "query") {
677
+ decRes = decRes.writeQueryValue(bodyPart.value);
678
+ }
679
+ }
680
+
681
+ return decRes.end();
682
+ } catch (e) {
683
+ return false;
684
+ }
685
+ };
686
+
687
+ /**
688
+ * Every node in the tree matching the predicate, in the order the named edges below are followed.
689
+ * The edges are written out rather than discovered, which is why compileDeclarative first refuses
690
+ * any node type that is not on the understood list: a shape this walk cannot see through would
691
+ * otherwise be compiled as though it were empty.
692
+ *
693
+ * @param {any} node
694
+ * @param {(node: any) => boolean} fn
695
+ * @returns {any[]}
696
+ */
697
+ function filterNodes(node, fn) {
698
+ const filtered = [];
699
+ if (fn(node)) {
700
+ filtered.push(node);
701
+ }
702
+ if (node.params) {
703
+ for (const param of node.params) {
704
+ filtered.push(...filterNodes(param, fn));
705
+ }
706
+ }
707
+
708
+ if (node.body) {
709
+ if (Array.isArray(node.body)) {
710
+ for (const child of node.body) {
711
+ filtered.push(...filterNodes(child, fn));
712
+ }
713
+ } else {
714
+ filtered.push(...filterNodes(node.body, fn));
715
+ }
716
+ }
717
+
718
+ if (node.declarations) {
719
+ for (const declaration of node.declarations) {
720
+ filtered.push(...filterNodes(declaration, fn));
721
+ }
722
+ }
723
+
724
+ if (node.expression) {
725
+ filtered.push(...filterNodes(node.expression, fn));
726
+ }
727
+
728
+ if (node.callee) {
729
+ filtered.push(...filterNodes(node.callee, fn));
730
+ }
731
+
732
+ if (node.object) {
733
+ filtered.push(...filterNodes(node.object, fn));
734
+ }
735
+
736
+ if (node.property) {
737
+ filtered.push(...filterNodes(node.property, fn));
738
+ }
739
+
740
+ if (node.id) {
741
+ filtered.push(...filterNodes(node.id, fn));
742
+ }
743
+ if (node.init) {
744
+ filtered.push(...filterNodes(node.init, fn));
745
+ }
746
+
747
+ if (node.left) {
748
+ filtered.push(...filterNodes(node.left, fn));
749
+ }
750
+ if (node.right) {
751
+ filtered.push(...filterNodes(node.right, fn));
752
+ }
753
+
754
+ if (node.arguments) {
755
+ for (const argument of node.arguments) {
756
+ filtered.push(...filterNodes(argument, fn));
757
+ }
758
+ }
759
+
760
+ // singular, and not the list above: it is what a return statement returns, and what a unary
761
+ // operator applies to. Without it `return res.send("x")` looked like a body with no calls in
762
+ // it at all, which would have compiled into an empty response.
763
+ if (node.argument) {
764
+ filtered.push(...filterNodes(node.argument, fn));
765
+ }
766
+
767
+ return filtered;
768
+ }