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.
- package/package.json +2 -1
- package/src/adopt.js +20 -26
- package/src/application.js +63 -73
- package/src/cli.js +48 -48
- package/src/cluster.js +18 -27
- package/src/compression.js +141 -112
- package/src/declarative.js +611 -540
- package/src/hot-settings.js +80 -0
- package/src/index.js +11 -17
- package/src/lazy-readable.js +131 -0
- package/src/lazy-writable.js +97 -0
- package/src/middlewares.js +153 -130
- package/src/nest.js +22 -36
- package/src/node-shim.js +19 -16
- package/src/optimizer.js +600 -0
- package/src/options.d.ts +9 -4
- package/src/parse-query.js +3 -3
- package/src/request-utils.js +307 -0
- package/src/request.js +147 -548
- package/src/response-utils.js +88 -0
- package/src/response.js +228 -535
- package/src/route.js +7 -8
- package/src/router-utils.js +998 -0
- package/src/router.js +166 -2170
- package/src/server-shape.js +40 -51
- package/src/server-timing.js +32 -33
- package/src/socket.js +208 -0
- package/src/testing.js +43 -45
- package/src/usage.js +25 -25
- package/src/utils.js +165 -78
- package/src/verify.js +22 -31
- package/src/view.js +6 -8
- package/src/walk.js +581 -0
- package/src/websocket.js +34 -26
- package/src/work.js +22 -28
package/src/declarative.js
CHANGED
|
@@ -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
|
|
23
|
-
//
|
|
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
|
|
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
|
-
//
|
|
53
|
-
//
|
|
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
|
-
//
|
|
58
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
67
|
-
//
|
|
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
|
|
94
|
-
*
|
|
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
|
|
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 {
|
|
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
|
|
135
|
-
*
|
|
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 {
|
|
142
|
-
* @returns {
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
297
|
-
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
if (
|
|
323
|
-
return
|
|
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
|
-
|
|
240
|
+
pairs.push([key, String(property.value.value)]);
|
|
326
241
|
}
|
|
327
242
|
} else {
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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
|
-
|
|
365
|
-
default:
|
|
366
|
-
return false;
|
|
271
|
+
}
|
|
367
272
|
}
|
|
368
|
-
|
|
369
|
-
if (
|
|
370
|
-
return
|
|
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
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
.
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
if (
|
|
444
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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 (
|
|
482
|
-
return
|
|
415
|
+
} else if (arg.type === "MemberExpression") {
|
|
416
|
+
if (!arg.object.property) {
|
|
417
|
+
return null;
|
|
483
418
|
}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
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
|
-
|
|
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
|
-
|
|
550
|
-
|
|
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
|
|
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
|
-
|
|
568
|
-
|
|
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
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
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
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
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
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
719
|
-
//
|
|
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
|
|
733
|
-
//
|
|
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
|
|
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
|
|
747
|
-
//
|
|
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
|
|
759
|
-
//
|
|
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
|
|
764
|
-
//
|
|
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
|
|
772
|
-
//
|
|
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
|
|
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
|
|
785
|
-
//
|
|
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
|
|
791
|
-
//
|
|
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
|
|
819
|
-
*
|
|
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
|
-
*
|
|
825
|
-
* @
|
|
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,
|
|
891
|
-
//
|
|
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
|
}
|