ultimate-express 1.3.2 → 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 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 | 94,296.49 | 108,551.92 | 104,756.22 | 69,581.33 |
47
- | hyper-express | 66,356.707 | 80,002.53 | 69,953.76 | 49,113.83 |
48
- | **ultimate-express** | **57,343.813** | **64,608.03** | **60,234.78** | **47,188.63** |
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:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ultimate-express",
3
- "version": "1.3.2",
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": {
@@ -42,6 +42,7 @@
42
42
  "dependencies": {
43
43
  "@types/express": "^4.0.0",
44
44
  "accepts": "^1.3.8",
45
+ "acorn": "^8.12.1",
45
46
  "bytes": "^3.1.2",
46
47
  "cookie": "^1.0.1",
47
48
  "cookie-signature": "^1.2.1",
@@ -58,7 +59,7 @@
58
59
  "statuses": "^2.0.1",
59
60
  "tseep": "^1.2.2",
60
61
  "type-is": "^1.6.18",
61
- "uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.48.0",
62
+ "uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.49.0",
62
63
  "vary": "^1.1.2"
63
64
  },
64
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
+ }
@@ -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 = decodeURIComponent(iq !== -1 ? req.url.substring(0, iq) : req.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/response.js CHANGED
@@ -539,8 +539,12 @@ module.exports = class Response extends Writable {
539
539
  }
540
540
  return this;
541
541
  }
542
- header = this.set;
543
- setHeader = this.set;
542
+ header(field, value) {
543
+ return this.set(field, value);
544
+ }
545
+ setHeader(field, value) {
546
+ return this.set(field, value);
547
+ }
544
548
  get(field) {
545
549
  return this.headers[field.toLowerCase()];
546
550
  }
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 { NullObject } = require("./utils.js");
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
 
@@ -268,7 +273,7 @@ module.exports = class Router extends EventEmitter {
268
273
  if(!route.optimizedRouter && route.path.includes(":")) {
269
274
  route.optimizedParams = route.path.match(regExParam).map(p => p.slice(1));
270
275
  }
271
- const fn = async (res, req) => {
276
+ let fn = async (res, req) => {
272
277
  const { request, response } = this.handleRequest(res, req);
273
278
  if(route.optimizedParams) {
274
279
  request.optimizedParams = new NullObject();
@@ -284,16 +289,36 @@ module.exports = class Router extends EventEmitter {
284
289
  }
285
290
  };
286
291
  route.optimizedPath = optimizedPath;
287
- let replacedPath = route.path.replace(regExParam, ':x');
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
+
288
313
  this.uwsApp[method](replacedPath, fn);
289
314
  if(!this.get('strict routing') && route.path[route.path.length - 1] !== '/') {
290
315
  this.uwsApp[method](replacedPath + '/', fn);
291
316
  if(method === 'get') {
292
- this.uwsApp.head(replacedPath + '/', fn);
317
+ this.uwsApp.head(replacedPath + '/', realFn);
293
318
  }
294
319
  }
295
320
  if(method === 'get') {
296
- this.uwsApp.head(replacedPath, fn);
321
+ this.uwsApp.head(replacedPath, realFn);
297
322
  }
298
323
  }
299
324
 
package/src/utils.js CHANGED
@@ -159,7 +159,8 @@ const defaultSettings = {
159
159
  'views': () => path.join(process.cwd(), 'views'),
160
160
  'view cache': () => process.env.NODE_ENV === 'production',
161
161
  'x-powered-by': true,
162
- 'case sensitive routing': true
162
+ 'case sensitive routing': true,
163
+ 'declarative responses': true
163
164
  };
164
165
 
165
166
  function compileTrust(val) {