ultimate-express 1.3.1 → 1.3.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.
- package/README.md +6 -6
- package/package.json +5 -3
- package/src/declarative.js +388 -0
- package/src/middlewares.js +9 -1
- package/src/request.js +24 -23
- package/src/response.js +15 -16
- package/src/router.js +59 -32
- package/src/utils.js +17 -11
package/README.md
CHANGED
|
@@ -43,9 +43,10 @@ For full table with other runtimes, check [here](https://github.com/dimdenGD/bun
|
|
|
43
43
|
|
|
44
44
|
| Framework | Average | Ping | Query | Body |
|
|
45
45
|
| -------------------- | -------------- | ------------- | ------------- | ------------- |
|
|
46
|
-
| uws |
|
|
47
|
-
|
|
|
48
|
-
|
|
|
46
|
+
| uws | 95,531.277 | 109,960.35 | 105,601.47 | 71,032.01 |
|
|
47
|
+
| **ultimate-express (declarative)** | **86,794.997** | **108,546.44** | **105,869.75** | **45,968.8** |
|
|
48
|
+
| hyper-express | 68,959.92 | 82,547.21 | 71,685.51 | 52,647.04 |
|
|
49
|
+
| **ultimate-express** | **60,839.75** | **68,938.53** | **66,173.86** | **47,406.86** |
|
|
49
50
|
| h3 | 35,423.263 | 41,243.68 | 34,429.26 | 30,596.85 |
|
|
50
51
|
| fastify | 33,094.62 | 40,147.67 | 40,076.35 | 19,059.84 |
|
|
51
52
|
| hono | 26,576.02 | 36,215.35 | 34,656.12 | 8,856.59 |
|
|
@@ -102,7 +103,7 @@ app.listen(3000, () => {
|
|
|
102
103
|
1. µExpress tries to optimize routing as much as possible, but it's only possible if:
|
|
103
104
|
- `case sensitive routing` is enabled (it is by default, unlike in normal Express).
|
|
104
105
|
- only string paths without regex characters like *, +, (), {}, etc. can be optimized.
|
|
105
|
-
- only 1-level deep routers can be optimized.
|
|
106
|
+
- only 1-level deep routers can be optimized.
|
|
106
107
|
|
|
107
108
|
Optimized routes can be up to 10 times faster than normal routes, as they're using native uWS router and have pre-calculated path.
|
|
108
109
|
|
|
@@ -114,8 +115,6 @@ Optimized routes can be up to 10 times faster than normal routes, as they're usi
|
|
|
114
115
|
|
|
115
116
|
5. By default, µExpress creates 1 (or 0 if your CPU has only 1 core) child thread to improve performance of reading files. You can change this number by setting `threads` to a different number in `express()`, or set to 0 to disable thread pool (`express({ threads: 0 })`). Threads are shared between all express() instances, with largest `threads` number being used. Using more threads will not necessarily improve performance. Sometimes not using threads at all is faster, please [test](https://github.com/wg/wrk/) both options.
|
|
116
117
|
|
|
117
|
-
6. Don't read `req.connection.remoteAddress` (or `req.ip` if `trust proxy` is disabled) after response is finished. In general, reading IP in uWS is quite slow (~15% slower), and you should only read it when you need it while request is still open. If you'll read it after response, it'll make µExpress read IP for every single request after, even when it's not needed.
|
|
118
|
-
|
|
119
118
|
## WebSockets
|
|
120
119
|
|
|
121
120
|
Since you don't create http server manually, you can't properly use http.on("upgrade") to handle WebSockets. To solve this, there's currently 2 options:
|
|
@@ -302,6 +301,7 @@ Almost all middlewares that are compatible with Express are compatible with µEx
|
|
|
302
301
|
- ✅ [express-rate-limit](https://npmjs.com/package/express-rate-limit)
|
|
303
302
|
- ✅ [express-subdomain](https://npmjs.com/package/express-subdomain)
|
|
304
303
|
- ✅ [vhost](https://npmjs.com/package/vhost)
|
|
304
|
+
- ✅ [tsoa](https://github.com/lukeautry/tsoa)
|
|
305
305
|
|
|
306
306
|
Middlewares and modules that are confirmed to not work:
|
|
307
307
|
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultimate-express",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.3",
|
|
4
4
|
"description": "The Ultimate Express. Fastest http server with full Express compatibility, based on uWebSockets.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"test": "node tests/index.js"
|
|
7
|
+
"test": "node tests/index.js",
|
|
8
|
+
"dev": "node --inspect=9229 demo/index.js"
|
|
8
9
|
},
|
|
9
10
|
"engines": {
|
|
10
11
|
"node": ">=16"
|
|
@@ -41,6 +42,7 @@
|
|
|
41
42
|
"dependencies": {
|
|
42
43
|
"@types/express": "^4.0.0",
|
|
43
44
|
"accepts": "^1.3.8",
|
|
45
|
+
"acorn": "^8.12.1",
|
|
44
46
|
"bytes": "^3.1.2",
|
|
45
47
|
"cookie": "^1.0.1",
|
|
46
48
|
"cookie-signature": "^1.2.1",
|
|
@@ -57,7 +59,7 @@
|
|
|
57
59
|
"statuses": "^2.0.1",
|
|
58
60
|
"tseep": "^1.2.2",
|
|
59
61
|
"type-is": "^1.6.18",
|
|
60
|
-
"uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.
|
|
62
|
+
"uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.49.0",
|
|
61
63
|
"vary": "^1.1.2"
|
|
62
64
|
},
|
|
63
65
|
"devDependencies": {
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
const acorn = require("acorn");
|
|
2
|
+
const uWS = require("uWebSockets.js");
|
|
3
|
+
|
|
4
|
+
const parser = acorn.Parser;
|
|
5
|
+
|
|
6
|
+
const allowedResMethods = ['set', 'header', 'setHeader', 'status', 'send', 'end', 'append'];
|
|
7
|
+
const allowedIdentifiers = ['query', 'params', ...allowedResMethods];
|
|
8
|
+
|
|
9
|
+
// generates a declarative response from a callback
|
|
10
|
+
// uWS allows creating such responses and they are extremely fast
|
|
11
|
+
// since you don't even have to call into Node.js at all
|
|
12
|
+
// declarative response will only be created if callback is 'simple enough'
|
|
13
|
+
// simple enough means:
|
|
14
|
+
// - doesnt call external functions
|
|
15
|
+
// - doesnt create variables
|
|
16
|
+
// - only uses req.query and req.params
|
|
17
|
+
// basically, its only simple, static responses
|
|
18
|
+
module.exports = function compileDeclarative(cb, app) {
|
|
19
|
+
try {
|
|
20
|
+
let code = cb.toString();
|
|
21
|
+
// convert anonymous functions to named ones to make it valid code
|
|
22
|
+
if(code.startsWith("function") || code.startsWith("async function")) {
|
|
23
|
+
code = code.replace(/function *\(/, "function __cb(");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const tokens = [...acorn.tokenizer(code, { ecmaVersion: "latest" })];
|
|
27
|
+
|
|
28
|
+
if(tokens.some(token => ['throw', 'new', 'await'].includes(token.value))) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const parsed = parser.parse(code, { ecmaVersion: "latest" }).body;
|
|
33
|
+
let fn = parsed[0];
|
|
34
|
+
|
|
35
|
+
if(fn.type === 'ExpressionStatement') {
|
|
36
|
+
fn = fn.expression;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// check if it is a function
|
|
40
|
+
if (fn.type !== 'FunctionDeclaration' && fn.type !== 'ArrowFunctionExpression') {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const args = fn.params.map(param => param.name);
|
|
45
|
+
|
|
46
|
+
if(args.length < 2) {
|
|
47
|
+
// invalid function? doesn't have (req, res) args
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const [req, res] = args;
|
|
52
|
+
let queryName, paramsName, queries = [], params = [];
|
|
53
|
+
|
|
54
|
+
if(fn.params[0].type === 'ObjectPattern') {
|
|
55
|
+
let query = fn.params[0].properties.find(prop => prop.key.name === 'query');
|
|
56
|
+
let param = fn.params[0].properties.find(prop => prop.key.name === 'params');
|
|
57
|
+
|
|
58
|
+
if(query?.value?.type === 'Identifier') {
|
|
59
|
+
queryName = query.value.name;
|
|
60
|
+
} else if(query?.value?.type === 'ObjectPattern') {
|
|
61
|
+
for(let prop of query.value.properties) {
|
|
62
|
+
if(prop.value.type !== 'Identifier') {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
queries.push(prop.value.name);
|
|
66
|
+
}
|
|
67
|
+
} else {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if(param?.value?.type === 'Identifier') {
|
|
72
|
+
paramsName = param.value.name;
|
|
73
|
+
} else if(param?.value?.type === 'ObjectPattern') {
|
|
74
|
+
for(let prop of param.value.properties) {
|
|
75
|
+
if(prop.value.type !== 'Identifier') {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
params.push(prop.value.name);
|
|
79
|
+
}
|
|
80
|
+
} else {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// check if it calls any other function other than the one in `res`
|
|
86
|
+
const callExprs = filterNodes(fn, node => node.type === 'CallExpression');
|
|
87
|
+
const resCalls = [];
|
|
88
|
+
for(let expr of callExprs) {
|
|
89
|
+
let calleeName, propertyName;
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
// get propertyName
|
|
93
|
+
if(expr.type === 'MemberExpression') {
|
|
94
|
+
propertyName = expr.property.name;
|
|
95
|
+
} else if(expr.type === 'CallExpression') {
|
|
96
|
+
propertyName = expr.callee?.property?.name ?? expr.callee?.name;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// get calleeName
|
|
100
|
+
switch(expr.callee.type) {
|
|
101
|
+
case "Identifier":
|
|
102
|
+
calleeName = expr.callee.name;
|
|
103
|
+
break;
|
|
104
|
+
case "MemberExpression":
|
|
105
|
+
if(expr.callee.object.type === 'Identifier') {
|
|
106
|
+
calleeName = expr.callee.object.name;
|
|
107
|
+
} else if(expr.callee.object.type === 'CallExpression') {
|
|
108
|
+
// function call chaining
|
|
109
|
+
let callee = expr.callee;
|
|
110
|
+
while(callee.object.callee) {
|
|
111
|
+
callee = callee.object.callee;
|
|
112
|
+
}
|
|
113
|
+
if(callee.object.type !== 'Identifier') {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
calleeName = callee.object.name;
|
|
117
|
+
}
|
|
118
|
+
break;
|
|
119
|
+
default:
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
// check if calleeName is res
|
|
123
|
+
if(calleeName !== res) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const obj = { calleeName, propertyName };
|
|
128
|
+
expr.obj = obj;
|
|
129
|
+
resCalls.push(obj);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// check if res property being called are
|
|
133
|
+
// - set, header, setHeader
|
|
134
|
+
// - status
|
|
135
|
+
// - send
|
|
136
|
+
// - end
|
|
137
|
+
for(let call of resCalls) {
|
|
138
|
+
if(!allowedResMethods.includes(call.propertyName)) {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// check if all identifiers are allowed
|
|
144
|
+
const identifiers = filterNodes(fn, node => node.type === 'Identifier').slice(args.length).map(id => id.name);
|
|
145
|
+
if(identifiers[identifiers.length - 1] === '__cb') {
|
|
146
|
+
identifiers.pop();
|
|
147
|
+
}
|
|
148
|
+
if(!identifiers.every((id, i) =>
|
|
149
|
+
allowedIdentifiers.includes(id) ||
|
|
150
|
+
id === req ||
|
|
151
|
+
id === res ||
|
|
152
|
+
(identifiers[i - 2] === req && identifiers[i - 1] === 'params') ||
|
|
153
|
+
(identifiers[i - 2] === req && identifiers[i - 1] === 'query') ||
|
|
154
|
+
id === queryName ||
|
|
155
|
+
id === paramsName ||
|
|
156
|
+
queries.includes(id) ||
|
|
157
|
+
params.includes(id)
|
|
158
|
+
)) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
let statusCode = 200;
|
|
164
|
+
const headers = [];
|
|
165
|
+
const body = [];
|
|
166
|
+
|
|
167
|
+
// get statusCode
|
|
168
|
+
for(let call of callExprs) {
|
|
169
|
+
if(call.obj.propertyName === 'status') {
|
|
170
|
+
if(call.arguments[0].type !== 'Literal') {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
statusCode = call.arguments[0].value;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// get headers
|
|
178
|
+
for(let call of callExprs) {
|
|
179
|
+
if(call.obj.propertyName === 'header' || call.obj.propertyName === 'setHeader' || call.obj.propertyName === 'set') {
|
|
180
|
+
if(call.arguments[0].type !== 'Literal' || call.arguments[1].type !== 'Literal') {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
const sameHeader = headers.find(header => header[0] === call.arguments[0].value);
|
|
184
|
+
if(sameHeader) {
|
|
185
|
+
sameHeader[1] = call.arguments[1].value;
|
|
186
|
+
} else {
|
|
187
|
+
headers.push([call.arguments[0].value, call.arguments[1].value]);
|
|
188
|
+
}
|
|
189
|
+
} else if(call.obj.propertyName === 'append') {
|
|
190
|
+
if(call.arguments[0].type !== 'Literal' || call.arguments[1].type !== 'Literal') {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
headers.push([call.arguments[0].value, call.arguments[1].value]);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// get body
|
|
198
|
+
for(let call of callExprs) {
|
|
199
|
+
if(call.obj.propertyName === 'send' || call.obj.propertyName === 'end') {
|
|
200
|
+
const arg = call.arguments[0];
|
|
201
|
+
if(arg) {
|
|
202
|
+
if(arg.type === 'Literal') {
|
|
203
|
+
if(typeof arg.value === 'number') { // status code
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
let val = arg.value;
|
|
207
|
+
if(val === null) {
|
|
208
|
+
val = '';
|
|
209
|
+
}
|
|
210
|
+
body.push({type: 'text', value: val});
|
|
211
|
+
} else if(arg.type === 'TemplateLiteral') {
|
|
212
|
+
const exprs = [...arg.quasis, ...arg.expressions].sort((a, b) => a.start - b.start);
|
|
213
|
+
for(let expr of exprs) {
|
|
214
|
+
if(expr.type === 'TemplateElement') {
|
|
215
|
+
body.push({type: 'text', value: expr.value.cooked});
|
|
216
|
+
} else if(expr.type === 'MemberExpression') {
|
|
217
|
+
const obj = expr.object;
|
|
218
|
+
let type;
|
|
219
|
+
if(obj.type === 'MemberExpression') {
|
|
220
|
+
if(obj.property.type !== 'Identifier') {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
type = obj.property.name;
|
|
224
|
+
} else if(obj.type === 'Identifier') {
|
|
225
|
+
type = obj.name;
|
|
226
|
+
} else {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
if(type !== 'params' && type !== 'query') {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
body.push({type, value: expr.property.name});
|
|
233
|
+
} else if(expr.type === 'Identifier') {
|
|
234
|
+
if(queries.includes(expr.name)) {
|
|
235
|
+
body.push({type: 'query', value: expr.name});
|
|
236
|
+
} else if(params.includes(expr.name)) {
|
|
237
|
+
body.push({type: 'params', value: expr.name});
|
|
238
|
+
} else {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
} else {
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
} else if(arg.type === 'MemberExpression') {
|
|
246
|
+
if(!arg.object.property) {
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
if(arg.object.property.type !== 'Identifier' || (arg.object.property.name !== 'query' && arg.object.property.name !== 'params')) {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
body.push({type: arg.object.property.name, value: arg.property.name});
|
|
253
|
+
} else if(arg.type === 'BinaryExpression') {
|
|
254
|
+
let stuff = [];
|
|
255
|
+
function check(node) {
|
|
256
|
+
if(node.right.type === 'Literal') {
|
|
257
|
+
stuff.push({type: 'text', value: node.right.value});
|
|
258
|
+
} else if(node.right.type === 'MemberExpression') {
|
|
259
|
+
stuff.push({type: node.right.object.property.name, value: node.right.property.name});
|
|
260
|
+
} else return false;
|
|
261
|
+
if(node.left.type === 'Literal') {
|
|
262
|
+
stuff.push({type: 'text', value: node.left.value});
|
|
263
|
+
} else if(node.left.type === 'MemberExpression') {
|
|
264
|
+
stuff.push({type: node.left.object.property.name, value: node.left.property.name});
|
|
265
|
+
} else if(node.left.type === 'BinaryExpression') {
|
|
266
|
+
return check(node.left);
|
|
267
|
+
} else return false;
|
|
268
|
+
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
if(!check(arg)) {
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
body.push(...stuff.reverse());
|
|
275
|
+
} else {
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// uws doesnt support status codes other than 200 currently
|
|
283
|
+
if(statusCode != 200) {
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
let decRes = new uWS.DeclarativeResponse();
|
|
288
|
+
|
|
289
|
+
for(let header of headers) {
|
|
290
|
+
if(header[0].toLowerCase() === 'content-length') {
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
decRes = decRes.writeHeader(header[0], header[1]);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if(app.get('etag') && !headers.some(header => header[0].toLowerCase() === 'etag')) {
|
|
297
|
+
if(body.some(part => part.type !== 'text')) {
|
|
298
|
+
return false;
|
|
299
|
+
} else {
|
|
300
|
+
decRes = decRes.writeHeader('ETag', app.get('etag fn')(body.map(part => part.value.toString()).join('')));
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if(app.get('x-powered-by')) {
|
|
305
|
+
decRes = decRes.writeHeader('x-powered-by', 'UltimateExpress');
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
for(let bodyPart of body) {
|
|
309
|
+
if(bodyPart.type === 'text' && String(bodyPart.value).length) {
|
|
310
|
+
decRes = decRes.write(String(bodyPart.value));
|
|
311
|
+
} else if(bodyPart.type === 'params') {
|
|
312
|
+
decRes = decRes.writeParameterValue(bodyPart.value);
|
|
313
|
+
} else if(bodyPart.type === 'query') {
|
|
314
|
+
decRes = decRes.writeQueryValue(bodyPart.value);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return decRes.end();
|
|
319
|
+
} catch(e) {
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function filterNodes(node, fn) {
|
|
325
|
+
const filtered = [];
|
|
326
|
+
if(fn(node)) {
|
|
327
|
+
filtered.push(node);
|
|
328
|
+
}
|
|
329
|
+
if(node.params) {
|
|
330
|
+
for(let param of node.params) {
|
|
331
|
+
filtered.push(...filterNodes(param, fn));
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if(node.body) {
|
|
336
|
+
if(Array.isArray(node.body)) {
|
|
337
|
+
for(let child of node.body) {
|
|
338
|
+
filtered.push(...filterNodes(child, fn));
|
|
339
|
+
}
|
|
340
|
+
} else {
|
|
341
|
+
filtered.push(...filterNodes(node.body, fn));
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if(node.declarations) {
|
|
346
|
+
for(let declaration of node.declarations) {
|
|
347
|
+
filtered.push(...filterNodes(declaration, fn));
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if(node.expression) {
|
|
352
|
+
filtered.push(...filterNodes(node.expression, fn));
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if(node.callee) {
|
|
356
|
+
filtered.push(...filterNodes(node.callee, fn));
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if(node.object) {
|
|
360
|
+
filtered.push(...filterNodes(node.object, fn));
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if(node.property) {
|
|
364
|
+
filtered.push(...filterNodes(node.property, fn));
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if(node.id) {
|
|
368
|
+
filtered.push(...filterNodes(node.id, fn));
|
|
369
|
+
}
|
|
370
|
+
if(node.init) {
|
|
371
|
+
filtered.push(...filterNodes(node.init, fn));
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if(node.left) {
|
|
375
|
+
filtered.push(...filterNodes(node.left, fn));
|
|
376
|
+
}
|
|
377
|
+
if(node.right) {
|
|
378
|
+
filtered.push(...filterNodes(node.right, fn));
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if(node.arguments) {
|
|
382
|
+
for(let argument of node.arguments) {
|
|
383
|
+
filtered.push(...filterNodes(argument, fn));
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return filtered;
|
|
388
|
+
}
|
package/src/middlewares.js
CHANGED
|
@@ -42,7 +42,15 @@ function static(root, options) {
|
|
|
42
42
|
|
|
43
43
|
return (req, res, next) => {
|
|
44
44
|
const iq = req.url.indexOf('?');
|
|
45
|
-
let url
|
|
45
|
+
let url;
|
|
46
|
+
try {
|
|
47
|
+
url = decodeURIComponent(iq !== -1 ? req.url.substring(0, iq) : req.url);
|
|
48
|
+
} catch(e) {
|
|
49
|
+
if(!options.fallthrough) {
|
|
50
|
+
res.status(404);
|
|
51
|
+
return next(new Error('Not found'));
|
|
52
|
+
} else return next();
|
|
53
|
+
}
|
|
46
54
|
let _path = url;
|
|
47
55
|
let fullpath = path.resolve(path.join(options.root, url));
|
|
48
56
|
if(options.root && !fullpath.startsWith(path.resolve(options.root))) {
|
package/src/request.js
CHANGED
|
@@ -351,53 +351,54 @@ module.exports = class Request extends Readable {
|
|
|
351
351
|
if(this.#cachedHeaders) {
|
|
352
352
|
return this.#cachedHeaders;
|
|
353
353
|
}
|
|
354
|
-
|
|
355
|
-
this.#rawHeadersEntries.
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
if(
|
|
354
|
+
this.#cachedHeaders = new NullObject();
|
|
355
|
+
for (let index = 0, len = this.#rawHeadersEntries.length; index < len; index++) {
|
|
356
|
+
let [key, value] = this.#rawHeadersEntries[index];
|
|
357
|
+
key = key.toLowerCase();
|
|
358
|
+
if(this.#cachedHeaders[key]) {
|
|
359
359
|
if(discardedDuplicates.includes(key)) {
|
|
360
|
-
|
|
360
|
+
continue;
|
|
361
361
|
}
|
|
362
362
|
if(key === 'cookie') {
|
|
363
|
-
|
|
363
|
+
this.#cachedHeaders[key] += '; ' + value;
|
|
364
364
|
} else if(key === 'set-cookie') {
|
|
365
|
-
|
|
365
|
+
this.#cachedHeaders[key].push(value);
|
|
366
366
|
} else {
|
|
367
|
-
|
|
367
|
+
this.#cachedHeaders[key] += ', ' + value;
|
|
368
368
|
}
|
|
369
|
-
|
|
369
|
+
continue;
|
|
370
370
|
}
|
|
371
371
|
if(key === 'set-cookie') {
|
|
372
|
-
|
|
372
|
+
this.#cachedHeaders[key] = [value];
|
|
373
373
|
} else {
|
|
374
|
-
|
|
374
|
+
this.#cachedHeaders[key] = value;
|
|
375
375
|
}
|
|
376
|
-
}
|
|
377
|
-
this.#cachedHeaders
|
|
378
|
-
return headers;
|
|
376
|
+
}
|
|
377
|
+
return this.#cachedHeaders;
|
|
379
378
|
}
|
|
380
379
|
|
|
381
380
|
get headersDistinct() {
|
|
382
381
|
if(this.#cachedDistinctHeaders) {
|
|
383
382
|
return this.#cachedDistinctHeaders;
|
|
384
383
|
}
|
|
385
|
-
|
|
384
|
+
this.#cachedDistinctHeaders = new NullObject();
|
|
386
385
|
this.#rawHeadersEntries.forEach((val) => {
|
|
387
|
-
|
|
388
|
-
|
|
386
|
+
const [key, value] = val;
|
|
387
|
+
if(!this.#cachedDistinctHeaders[key]) {
|
|
388
|
+
this.#cachedDistinctHeaders[key] = [value];
|
|
389
|
+
return;
|
|
389
390
|
}
|
|
390
|
-
|
|
391
|
+
this.#cachedDistinctHeaders[key].push(value);
|
|
391
392
|
});
|
|
392
|
-
this.#cachedDistinctHeaders
|
|
393
|
-
return headers;
|
|
393
|
+
return this.#cachedDistinctHeaders;
|
|
394
394
|
}
|
|
395
395
|
|
|
396
396
|
get rawHeaders() {
|
|
397
397
|
const res = [];
|
|
398
|
-
this.#rawHeadersEntries.
|
|
398
|
+
for (let index = 0, len = this.#rawHeadersEntries.length; index < len; index++) {
|
|
399
|
+
const val = this.#rawHeadersEntries[index];
|
|
399
400
|
res.push(val[0], val[1]);
|
|
400
|
-
}
|
|
401
|
+
}
|
|
401
402
|
return res;
|
|
402
403
|
}
|
|
403
404
|
}
|
package/src/response.js
CHANGED
|
@@ -225,7 +225,7 @@ module.exports = class Response extends Writable {
|
|
|
225
225
|
this._res.cork(() => {
|
|
226
226
|
if(!this.headersSent) {
|
|
227
227
|
const etagFn = this.app.get('etag fn');
|
|
228
|
-
if(data && !this.headers['etag'] &&
|
|
228
|
+
if(etagFn && data && !this.headers['etag'] && !this.req.noEtag) {
|
|
229
229
|
this.headers['etag'] = etagFn(data);
|
|
230
230
|
}
|
|
231
231
|
const fresh = this.req.fresh;
|
|
@@ -238,8 +238,9 @@ module.exports = class Response extends Writable {
|
|
|
238
238
|
return;
|
|
239
239
|
}
|
|
240
240
|
}
|
|
241
|
-
|
|
242
|
-
|
|
241
|
+
const contentLength = this.headers['content-length'];
|
|
242
|
+
if(!data && contentLength) {
|
|
243
|
+
this._res.endWithoutBody(contentLength.toString());
|
|
243
244
|
} else {
|
|
244
245
|
if(data instanceof Buffer) {
|
|
245
246
|
data = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
|
|
@@ -279,8 +280,8 @@ module.exports = class Response extends Writable {
|
|
|
279
280
|
}
|
|
280
281
|
if(typeof body === 'string') {
|
|
281
282
|
const contentType = this.headers['content-type'];
|
|
282
|
-
if(!contentType){
|
|
283
|
-
this.type
|
|
283
|
+
if(!contentType) {
|
|
284
|
+
this.headers['content-type'] = 'text/html; charset=utf-8';
|
|
284
285
|
} else if(!contentType.includes(';')) {
|
|
285
286
|
this.headers['content-type'] += '; charset=utf-8';
|
|
286
287
|
}
|
|
@@ -361,7 +362,8 @@ module.exports = class Response extends Writable {
|
|
|
361
362
|
this.status(403);
|
|
362
363
|
return done(new Error('Forbidden'));
|
|
363
364
|
case 'ignore_files':
|
|
364
|
-
|
|
365
|
+
const len = parts.length;
|
|
366
|
+
if(len > 1 && parts[len - 1].startsWith('.')) {
|
|
365
367
|
this.status(404);
|
|
366
368
|
return done(new Error('Not found'));
|
|
367
369
|
}
|
|
@@ -408,7 +410,7 @@ module.exports = class Response extends Writable {
|
|
|
408
410
|
}
|
|
409
411
|
|
|
410
412
|
// etag
|
|
411
|
-
if(options.etag && !this.headers['etag']
|
|
413
|
+
if(options.etag && etagFn && !this.headers['etag']) {
|
|
412
414
|
this.headers['etag'] = etagFn(stat);
|
|
413
415
|
}
|
|
414
416
|
if(!options.etag) {
|
|
@@ -442,9 +444,10 @@ module.exports = class Response extends Writable {
|
|
|
442
444
|
}
|
|
443
445
|
if(ranges !== -2 && ranges.length === 1) {
|
|
444
446
|
this.status(206);
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
447
|
+
const range = ranges[0];
|
|
448
|
+
this.headers['content-range'] = `bytes ${range.start}-${range.end}/${stat.size}`;
|
|
449
|
+
offset = range.start;
|
|
450
|
+
len = range.end - range.start + 1;
|
|
448
451
|
ranged = true;
|
|
449
452
|
}
|
|
450
453
|
}
|
|
@@ -545,9 +548,7 @@ module.exports = class Response extends Writable {
|
|
|
545
548
|
get(field) {
|
|
546
549
|
return this.headers[field.toLowerCase()];
|
|
547
550
|
}
|
|
548
|
-
getHeader
|
|
549
|
-
return this.get(field);
|
|
550
|
-
}
|
|
551
|
+
getHeader = this.get;
|
|
551
552
|
removeHeader(field) {
|
|
552
553
|
delete this.headers[field.toLowerCase()];
|
|
553
554
|
return this;
|
|
@@ -709,9 +710,7 @@ module.exports = class Response extends Writable {
|
|
|
709
710
|
|
|
710
711
|
return this.set('content-type', ct);
|
|
711
712
|
}
|
|
712
|
-
contentType
|
|
713
|
-
return this.type(type);
|
|
714
|
-
}
|
|
713
|
+
contentType = this.type;
|
|
715
714
|
|
|
716
715
|
vary(field) {
|
|
717
716
|
vary(this, field);
|
package/src/router.js
CHANGED
|
@@ -14,11 +14,16 @@ See the License for the specific language governing permissions and
|
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
const { patternToRegex, needsConversionToRegex, deprecated, findIndexStartingFrom, canBeOptimized } = require("./utils.js");
|
|
17
|
+
const { patternToRegex, needsConversionToRegex, deprecated, findIndexStartingFrom, canBeOptimized, NullObject } = require("./utils.js");
|
|
18
18
|
const Response = require("./response.js");
|
|
19
19
|
const Request = require("./request.js");
|
|
20
20
|
const { EventEmitter } = require("tseep");
|
|
21
|
-
const
|
|
21
|
+
const compileDeclarative = require("./declarative.js");
|
|
22
|
+
|
|
23
|
+
let resCodes = {}, resDecMethods = ['set', 'setHeader', 'header', 'send', 'end', 'append', 'status'];
|
|
24
|
+
for(let method of resDecMethods) {
|
|
25
|
+
resCodes[method] = Response.prototype[method].toString();
|
|
26
|
+
}
|
|
22
27
|
|
|
23
28
|
let routeKey = 0;
|
|
24
29
|
|
|
@@ -31,6 +36,8 @@ const methods = [
|
|
|
31
36
|
];
|
|
32
37
|
const supportedUwsMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', 'HEAD', 'CONNECT', 'TRACE'];
|
|
33
38
|
|
|
39
|
+
const regExParam = /:(\w+)/g;
|
|
40
|
+
|
|
34
41
|
module.exports = class Router extends EventEmitter {
|
|
35
42
|
constructor(settings = {}) {
|
|
36
43
|
super();
|
|
@@ -264,9 +271,9 @@ module.exports = class Router extends EventEmitter {
|
|
|
264
271
|
method = 'del';
|
|
265
272
|
}
|
|
266
273
|
if(!route.optimizedRouter && route.path.includes(":")) {
|
|
267
|
-
route.optimizedParams = route.path.match(
|
|
274
|
+
route.optimizedParams = route.path.match(regExParam).map(p => p.slice(1));
|
|
268
275
|
}
|
|
269
|
-
|
|
276
|
+
let fn = async (res, req) => {
|
|
270
277
|
const { request, response } = this.handleRequest(res, req);
|
|
271
278
|
if(route.optimizedParams) {
|
|
272
279
|
request.optimizedParams = new NullObject();
|
|
@@ -282,16 +289,36 @@ module.exports = class Router extends EventEmitter {
|
|
|
282
289
|
}
|
|
283
290
|
};
|
|
284
291
|
route.optimizedPath = optimizedPath;
|
|
285
|
-
|
|
292
|
+
|
|
293
|
+
let replacedPath = route.path;
|
|
294
|
+
const realFn = fn;
|
|
295
|
+
|
|
296
|
+
// check if route is declarative
|
|
297
|
+
if(
|
|
298
|
+
optimizedPath.length === 1 && // must not have middlewares
|
|
299
|
+
route.callbacks.length === 1 && // must not have multiple callbacks
|
|
300
|
+
typeof route.callbacks[0] === 'function' && // must be a function
|
|
301
|
+
this._paramCallbacks.size === 0 && // app.param() is not supported
|
|
302
|
+
!resDecMethods.some(method => resCodes[method] !== this.response[method].toString()) && // must not have injected methods
|
|
303
|
+
this.get('declarative responses') // must have declarative responses enabled
|
|
304
|
+
) {
|
|
305
|
+
const decRes = compileDeclarative(route.callbacks[0], this);
|
|
306
|
+
if(decRes) {
|
|
307
|
+
fn = decRes;
|
|
308
|
+
}
|
|
309
|
+
} else {
|
|
310
|
+
replacedPath = route.path.replace(regExParam, ':x');
|
|
311
|
+
}
|
|
312
|
+
|
|
286
313
|
this.uwsApp[method](replacedPath, fn);
|
|
287
314
|
if(!this.get('strict routing') && route.path[route.path.length - 1] !== '/') {
|
|
288
315
|
this.uwsApp[method](replacedPath + '/', fn);
|
|
289
316
|
if(method === 'get') {
|
|
290
|
-
this.uwsApp.head(replacedPath + '/',
|
|
317
|
+
this.uwsApp.head(replacedPath + '/', realFn);
|
|
291
318
|
}
|
|
292
319
|
}
|
|
293
320
|
if(method === 'get') {
|
|
294
|
-
this.uwsApp.head(replacedPath,
|
|
321
|
+
this.uwsApp.head(replacedPath, realFn);
|
|
295
322
|
}
|
|
296
323
|
}
|
|
297
324
|
|
|
@@ -407,33 +434,33 @@ module.exports = class Router extends EventEmitter {
|
|
|
407
434
|
}
|
|
408
435
|
|
|
409
436
|
async _routeRequest(req, res, startIndex = 0, routes = this._routes, skipCheck = false, skipUntil) {
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
if(!
|
|
414
|
-
if
|
|
415
|
-
|
|
416
|
-
return resolve(false);
|
|
417
|
-
}
|
|
418
|
-
// on optimized routes, there can be more routes, so we have to use unoptimized routing and skip until we find route we stopped at
|
|
419
|
-
return resolve(this._routeRequest(req, res, 0, this._routes, false, skipUntil));
|
|
437
|
+
let routeIndex = skipCheck ? startIndex : findIndexStartingFrom(routes, r => (r.all || r.method === req.method || (r.gettable && req.method === 'HEAD')) && this._pathMatches(r, req), startIndex);
|
|
438
|
+
const route = routes[routeIndex];
|
|
439
|
+
if(!route) {
|
|
440
|
+
if(!skipCheck) {
|
|
441
|
+
// on normal unoptimized routes, if theres no match then there is no route
|
|
442
|
+
return false;
|
|
420
443
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
444
|
+
// on optimized routes, there can be more routes, so we have to use unoptimized routing and skip until we find route we stopped at
|
|
445
|
+
return this._routeRequest(req, res, 0, this._routes, false, skipUntil);
|
|
446
|
+
}
|
|
447
|
+
let callbackindex = 0;
|
|
448
|
+
const continueRoute = await this._preprocessRequest(req, res, route);
|
|
449
|
+
if(route.use) {
|
|
450
|
+
const strictRouting = this.get('strict routing');
|
|
451
|
+
req._stack.push(route.path);
|
|
452
|
+
req._opPath = req.path.replace(this.getFullMountpath(req), '');
|
|
453
|
+
if(strictRouting) {
|
|
454
|
+
if(req.endsWithSlash && req.path !== '/') {
|
|
455
|
+
req._opPath += '/';
|
|
433
456
|
}
|
|
434
|
-
|
|
435
|
-
|
|
457
|
+
} else if(req.endsWithSlash && req.path !== '/') {
|
|
458
|
+
req._opPath = req._opPath.slice(0, -1);
|
|
436
459
|
}
|
|
460
|
+
req.url = req._opPath + req.urlQuery;
|
|
461
|
+
if(req.url === '') req.url = '/';
|
|
462
|
+
}
|
|
463
|
+
return new Promise((resolve) => {
|
|
437
464
|
const next = async (thingamabob) => {
|
|
438
465
|
if(thingamabob) {
|
|
439
466
|
if(thingamabob === 'route' || thingamabob === 'skipPop') {
|
|
@@ -565,4 +592,4 @@ module.exports = class Router extends EventEmitter {
|
|
|
565
592
|
`</body>\n` +
|
|
566
593
|
`</html>\n`);
|
|
567
594
|
}
|
|
568
|
-
}
|
|
595
|
+
}
|
package/src/utils.js
CHANGED
|
@@ -23,7 +23,11 @@ const etag = require("etag");
|
|
|
23
23
|
const { Stats } = require("fs");
|
|
24
24
|
|
|
25
25
|
function fastQueryParse(query, options) {
|
|
26
|
-
|
|
26
|
+
const len = query.length;
|
|
27
|
+
if(len === 0){
|
|
28
|
+
return new NullObject();
|
|
29
|
+
}
|
|
30
|
+
if(len <= 128) {
|
|
27
31
|
if(!query.includes('[') && !query.includes('%5B') && !query.includes('.') && !query.includes('%2E')) {
|
|
28
32
|
return querystring.parse(query);
|
|
29
33
|
}
|
|
@@ -44,9 +48,9 @@ function patternToRegex(pattern, isPrefix = false) {
|
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
let regexPattern = pattern
|
|
47
|
-
.
|
|
48
|
-
.
|
|
49
|
-
.
|
|
51
|
+
.replaceAll('.', '\\.')
|
|
52
|
+
.replaceAll('-', '\\-')
|
|
53
|
+
.replaceAll('*', '(.*)') // Convert * to .*
|
|
50
54
|
.replace(/:(\w+)(\(.+?\))?/g, (match, param, regex) => {
|
|
51
55
|
return `(?<${param}>${regex ? regex + '($|\\/)' : '[^/]+'})`;
|
|
52
56
|
}); // Convert :param to capture group
|
|
@@ -101,12 +105,13 @@ function acceptParams(str) {
|
|
|
101
105
|
const parts = str.split(/ *; */);
|
|
102
106
|
const ret = { value: parts[0], quality: 1, params: {} }
|
|
103
107
|
|
|
104
|
-
for (let i = 1
|
|
108
|
+
for (let i = 1, len = parts.length; i < len; ++i) {
|
|
105
109
|
const pms = parts[i].split(/ *= */);
|
|
106
|
-
|
|
107
|
-
|
|
110
|
+
const [pms_0, pms_1] = pms;
|
|
111
|
+
if ('q' === pms_0) {
|
|
112
|
+
ret.quality = parseFloat(pms_1);
|
|
108
113
|
} else {
|
|
109
|
-
ret.params[
|
|
114
|
+
ret.params[pms_0] = pms_1;
|
|
110
115
|
}
|
|
111
116
|
}
|
|
112
117
|
|
|
@@ -154,7 +159,8 @@ const defaultSettings = {
|
|
|
154
159
|
'views': () => path.join(process.cwd(), 'views'),
|
|
155
160
|
'view cache': () => process.env.NODE_ENV === 'production',
|
|
156
161
|
'x-powered-by': true,
|
|
157
|
-
'case sensitive routing': true
|
|
162
|
+
'case sensitive routing': true,
|
|
163
|
+
'declarative responses': true
|
|
158
164
|
};
|
|
159
165
|
|
|
160
166
|
function compileTrust(val) {
|
|
@@ -218,7 +224,7 @@ function decode (path) {
|
|
|
218
224
|
const UP_PATH_REGEXP = /(?:^|[\\/])\.\.(?:[\\/]|$)/;
|
|
219
225
|
|
|
220
226
|
function containsDotFile(parts) {
|
|
221
|
-
for(let i = 0
|
|
227
|
+
for(let i = 0, len = parts.length; i < len; i++) {
|
|
222
228
|
const part = parts[i];
|
|
223
229
|
if(part.length > 1 && part[0] === '.') {
|
|
224
230
|
return true;
|
|
@@ -341,4 +347,4 @@ module.exports = {
|
|
|
341
347
|
findIndexStartingFrom,
|
|
342
348
|
fastQueryParse,
|
|
343
349
|
canBeOptimized
|
|
344
|
-
};
|
|
350
|
+
};
|