ultimate-express 1.2.30 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -16,7 +16,7 @@ To make sure µExpress matches behavior of Express in all cases, we run all test
16
16
 
17
17
  Similar projects based on uWebSockets:
18
18
 
19
- - `express` on Bun - since Bun uses uWS for its HTTP module, Express is about 2-3 times faster than on Node.js, but still slower than µExpress because it doesn't do uWS-specific optimizations.
19
+ - `express` on Bun - since Bun uses uWS for its HTTP module, Express is about 2-3 times faster than on Node.js, but still almost 2 times slower than µExpress because it doesn't do uWS-specific optimizations.
20
20
  - `hyper-express` - while having a similar API to Express, it's very far from being a drop-in replacement, and implements most of the functionality differently. This creates a lot of random quirks and issues, making the switch quite difficult. Built in middlewares are also very different, middlewares for Express are mostly not supported.
21
21
  - `uwebsockets-express` - this library is closer to being a drop-in replacement, but misses a lot of APIs, depends on Express by calling it's methods under the hood and doesn't try to optimize routing by using native uWS router.
22
22
 
@@ -61,6 +61,7 @@ Also tested on a [real-world application](https://nekoweb.org) with templates, s
61
61
  In a lot of cases, you can just replace `require("express")` with `require("ultimate-express")` and everything works the same. But there are some differences:
62
62
 
63
63
  - `case sensitive routing` is enabled by default.
64
+ - a new option `catch async errors` is added. If it's enabled, you don't need to use `express-async-errors` module.
64
65
  - request body is only read for POST, PUT and PATCH requests by default. You can add additional methods by setting `body methods` to array with uppercased methods.
65
66
  - For HTTPS, instead of doing this:
66
67
  ```js
@@ -288,6 +289,7 @@ Almost all middlewares that are compatible with Express are compatible with µEx
288
289
  - ✅ [body-parser](https://npmjs.com/package/body-parser) (use `express.text()` etc instead for better performance)
289
290
  - ✅ [cookie-parser](https://npmjs.com/package/cookie-parser)
290
291
  - ✅ [cookie-session](https://npmjs.com/package/cookie-session)
292
+ - ✅ [compression](https://npmjs.com/package/compression)
291
293
  - ✅ [serve-static](https://npmjs.com/package/serve-static) (use `express.static()` instead for better performance)
292
294
  - ✅ [serve-index](https://npmjs.com/package/serve-index)
293
295
  - ✅ [cors](https://npmjs.com/package/cors)
@@ -301,9 +303,9 @@ Almost all middlewares that are compatible with Express are compatible with µEx
301
303
  - ✅ [express-subdomain](https://npmjs.com/package/express-subdomain)
302
304
  - ✅ [vhost](https://npmjs.com/package/vhost)
303
305
 
304
- Middlewares that are confirmed to not work:
306
+ Middlewares and modules that are confirmed to not work:
305
307
 
306
- - ❌ [compression](https://npmjs.com/package/compression) (doesn't error, but doesn't compress)
308
+ - ❌ [express-async-errors](https://npmjs.com/package/express-async-errors) - doesn't work, use `app.set('catch async errors', true)` instead.
307
309
 
308
310
  ## Tested view engines
309
311
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ultimate-express",
3
- "version": "1.2.30",
3
+ "version": "1.3.1",
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": {
@@ -31,6 +31,7 @@
31
31
  "uwebsockets",
32
32
  "uws"
33
33
  ],
34
+ "types": "src/types.d.ts",
34
35
  "author": "dimden.dev",
35
36
  "license": "Apache-2.0",
36
37
  "bugs": {
@@ -38,9 +39,10 @@
38
39
  },
39
40
  "homepage": "https://github.com/dimdenGD/ultimate-express#readme",
40
41
  "dependencies": {
42
+ "@types/express": "^4.0.0",
41
43
  "accepts": "^1.3.8",
42
44
  "bytes": "^3.1.2",
43
- "cookie": "^0.6.0",
45
+ "cookie": "^1.0.1",
44
46
  "cookie-signature": "^1.2.1",
45
47
  "encodeurl": "^2.0.0",
46
48
  "etag": "^1.8.1",
@@ -60,6 +62,7 @@
60
62
  },
61
63
  "devDependencies": {
62
64
  "body-parser": "^1.20.3",
65
+ "compression": "^1.7.4",
63
66
  "cookie-parser": "^1.4.6",
64
67
  "cookie-session": "^2.1.0",
65
68
  "cors": "^2.8.5",
@@ -68,6 +71,7 @@
68
71
  "exit-hook": "^2.2.1",
69
72
  "express": "^4.19.2",
70
73
  "express-art-template": "^1.0.1",
74
+ "express-async-errors": "^3.1.1",
71
75
  "express-dot-engine": "^1.0.8",
72
76
  "express-fileupload": "^1.5.1",
73
77
  "express-handlebars": "^8.0.1",
@@ -78,6 +82,7 @@
78
82
  "multer": "^1.4.5-lts.1",
79
83
  "mustache-express": "^1.3.2",
80
84
  "pako": "^2.1.0",
85
+ "pkg-pr-new": "^0.0.29",
81
86
  "pug": "^3.0.3",
82
87
  "response-time": "^2.3.2",
83
88
  "serve-index": "^1.9.1",
@@ -16,7 +16,7 @@ limitations under the License.
16
16
 
17
17
  const uWS = require("uWebSockets.js");
18
18
  const Router = require("./router.js");
19
- const { removeDuplicateSlashes, defaultSettings, compileTrust, createETagGenerator, fastQueryParse } = require("./utils.js");
19
+ const { removeDuplicateSlashes, defaultSettings, compileTrust, createETagGenerator, fastQueryParse, NullObject } = require("./utils.js");
20
20
  const querystring = require("fast-querystring");
21
21
  const ViewClass = require("./view.js");
22
22
  const path = require("path");
@@ -27,7 +27,7 @@ const cpuCount = os.cpus().length;
27
27
 
28
28
  let workers = [];
29
29
  let taskKey = 0;
30
- const workerTasks = {};
30
+ const workerTasks = new NullObject();
31
31
 
32
32
  function createWorker() {
33
33
  const worker = new Worker(path.join(__dirname, 'worker.js'));
@@ -47,7 +47,7 @@ function createWorker() {
47
47
  }
48
48
 
49
49
  class Application extends Router {
50
- constructor(settings = {}) {
50
+ constructor(settings = new NullObject()) {
51
51
  super(settings);
52
52
  if(!settings?.uwsOptions) {
53
53
  settings.uwsOptions = {};
@@ -62,8 +62,8 @@ class Application extends Router {
62
62
  this.uwsApp = uWS.App(settings.uwsOptions);
63
63
  this.ssl = false;
64
64
  }
65
- this.cache = {};
66
- this.engines = {};
65
+ this.cache = new NullObject();
66
+ this.engines = new NullObject();
67
67
  this.locals = {
68
68
  settings: this.settings
69
69
  };
@@ -159,12 +159,12 @@ class Application extends Router {
159
159
  }
160
160
 
161
161
  enable(key) {
162
- this.settings[key] = true;
162
+ this.set(key, true);
163
163
  return this;
164
164
  }
165
165
 
166
166
  disable(key) {
167
- this.settings[key] = false;
167
+ this.set(key, false);
168
168
  return this;
169
169
  }
170
170
 
@@ -206,7 +206,7 @@ class Application extends Router {
206
206
  throw err;
207
207
  }
208
208
  this.port = uWS.us_socket_local_port(socket);
209
- callback(this.port);
209
+ if(callback) callback(this.port);
210
210
  };
211
211
  let fn = 'listen';
212
212
  let args = [];
@@ -229,6 +229,7 @@ class Application extends Router {
229
229
  }
230
230
  this.listenCalled = true;
231
231
  this.uwsApp[fn](...args);
232
+ return this.uwsApp;
232
233
  }
233
234
 
234
235
  address() {
@@ -260,10 +261,10 @@ class Application extends Router {
260
261
  render(name, options, callback) {
261
262
  if(typeof options === 'function') {
262
263
  callback = options;
263
- options = {};
264
+ options = new NullObject();
264
265
  }
265
266
  if(!options) {
266
- options = {};
267
+ options = new NullObject();
267
268
  } else {
268
269
  options = Object.assign({}, options);
269
270
  }
@@ -318,4 +319,4 @@ class Application extends Router {
318
319
 
319
320
  module.exports = function(options) {
320
321
  return new Application(options);
321
- }
322
+ }
@@ -20,10 +20,10 @@ const bytes = require('bytes');
20
20
  const zlib = require('fast-zlib');
21
21
  const typeis = require('type-is');
22
22
  const querystring = require('fast-querystring');
23
- const { fastQueryParse } = require('./utils.js');
23
+ const { fastQueryParse, NullObject } = require('./utils.js');
24
24
 
25
25
  function static(root, options) {
26
- if(!options) options = {};
26
+ if(!options) options = new NullObject();
27
27
  if(typeof options.index === 'undefined') options.index = 'index.html';
28
28
  if(typeof options.redirect === 'undefined') options.redirect = true;
29
29
  if(typeof options.fallthrough === 'undefined') options.fallthrough = true;
@@ -132,7 +132,7 @@ function createInflate(contentEncoding) {
132
132
  function createBodyParser(defaultType, beforeReturn) {
133
133
  return function(options) {
134
134
  if(typeof options !== 'object') {
135
- options = {};
135
+ options = new NullObject();
136
136
  }
137
137
  if(typeof options.limit === 'undefined') options.limit = bytes('100kb');
138
138
  else options.limit = bytes(options.limit);
@@ -160,7 +160,7 @@ function createBodyParser(defaultType, beforeReturn) {
160
160
  return next();
161
161
  }
162
162
 
163
- req.body = {};
163
+ req.body = new NullObject();
164
164
 
165
165
  // skip reading body for non-json content type
166
166
  if(!type) {
package/src/request.js CHANGED
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */
16
16
 
17
- const { patternToRegex, deprecated } = require("./utils.js");
17
+ const { patternToRegex, deprecated, NullObject } = require("./utils.js");
18
18
  const accepts = require("accepts");
19
19
  const typeis = require("type-is");
20
20
  const parseRange = require("range-parser");
@@ -64,7 +64,7 @@ module.exports = class Request extends Readable {
64
64
  this._opPath = this._opPath.slice(0, -1);
65
65
  }
66
66
  this.method = req.getCaseSensitiveMethod().toUpperCase();
67
- this.params = {};
67
+ this.params = new NullObject();
68
68
 
69
69
  this._gotParams = new Set();
70
70
  this._stack = [];
@@ -206,7 +206,7 @@ module.exports = class Request extends Readable {
206
206
  if(qp) {
207
207
  this.#cachedQuery = qp(this.urlQuery.slice(1));
208
208
  } else {
209
- this.#cachedQuery = {};
209
+ this.#cachedQuery = new NullObject();
210
210
  }
211
211
  return this.#cachedQuery;
212
212
  }
@@ -351,7 +351,7 @@ module.exports = class Request extends Readable {
351
351
  if(this.#cachedHeaders) {
352
352
  return this.#cachedHeaders;
353
353
  }
354
- let headers = {};
354
+ let headers = new NullObject();
355
355
  this.#rawHeadersEntries.forEach((val) => {
356
356
  const key = val[0].toLowerCase();
357
357
  const value = val[1];
@@ -382,7 +382,7 @@ module.exports = class Request extends Readable {
382
382
  if(this.#cachedDistinctHeaders) {
383
383
  return this.#cachedDistinctHeaders;
384
384
  }
385
- let headers = {};
385
+ let headers = new NullObject();
386
386
  this.#rawHeadersEntries.forEach((val) => {
387
387
  if(!headers[val[0]]) {
388
388
  headers[val[0]] = [];
package/src/response.js CHANGED
@@ -20,7 +20,7 @@ const vary = require("vary");
20
20
  const encodeUrl = require("encodeurl");
21
21
  const {
22
22
  normalizeType, stringify, deprecated, UP_PATH_REGEXP, decode,
23
- containsDotFile, isPreconditionFailure, isRangeFresh, parseHttpDate
23
+ containsDotFile, isPreconditionFailure, isRangeFresh, NullObject
24
24
  } = require("./utils.js");
25
25
  const { Writable } = require("stream");
26
26
  const { isAbsolute } = require("path");
@@ -70,7 +70,7 @@ module.exports = class Response extends Writable {
70
70
  this._res = res;
71
71
  this.headersSent = false;
72
72
  this.app = app;
73
- this.locals = {};
73
+ this.locals = new NullObject();
74
74
  this.finished = false;
75
75
  this.aborted = false;
76
76
  this.statusCode = 200;
@@ -208,12 +208,9 @@ module.exports = class Response extends Writable {
208
208
  _implicitHeader() {
209
209
  // compatibility function
210
210
  // usually should send headers but this is useless for us
211
- return;
211
+ this.writeHead(this.statusCode);
212
212
  }
213
213
  status(code) {
214
- if(this.headersSent) {
215
- throw new Error('Can\'t set status: Response was already sent');
216
- }
217
214
  this.statusCode = parseInt(code);
218
215
  return this;
219
216
  }
@@ -224,7 +221,7 @@ module.exports = class Response extends Writable {
224
221
  if(this.finished) {
225
222
  return;
226
223
  }
227
-
224
+ this.writeHead(this.statusCode);
228
225
  this._res.cork(() => {
229
226
  if(!this.headersSent) {
230
227
  const etagFn = this.app.get('etag fn');
@@ -282,22 +279,23 @@ module.exports = class Response extends Writable {
282
279
  }
283
280
  if(typeof body === 'string') {
284
281
  const contentType = this.headers['content-type'];
285
- if(contentType && !contentType.includes(';')) {
282
+ if(!contentType){
283
+ this.type('html'); // string defaulting to html
284
+ } else if(!contentType.includes(';')) {
286
285
  this.headers['content-type'] += '; charset=utf-8';
287
286
  }
288
287
  }
289
- this.writeHead(this.statusCode);
290
288
  return this.end(body);
291
289
  }
292
- sendFile(path, options = {}, callback) {
290
+ sendFile(path, options = new NullObject(), callback) {
293
291
  if(typeof path !== 'string') {
294
292
  throw new TypeError('path argument is required to res.sendFile');
295
293
  }
296
294
  if(typeof options === 'function') {
297
295
  callback = options;
298
- options = {};
296
+ options = new NullObject();
299
297
  }
300
- if(!options) options = {};
298
+ if(!options) options = new NullObject();
301
299
  let done = callback;
302
300
  if(!done) done = this.req.next;
303
301
  // default options
@@ -489,16 +487,16 @@ module.exports = class Response extends Writable {
489
487
  download(path, filename, options, callback) {
490
488
  let done = callback;
491
489
  let name = filename;
492
- let opts = options || {};
490
+ let opts = options || new NullObject();
493
491
 
494
492
  // support function as second or third arg
495
493
  if (typeof filename === 'function') {
496
494
  done = filename;
497
495
  name = null;
498
- opts = {};
496
+ opts = new NullObject();
499
497
  } else if (typeof options === 'function') {
500
498
  done = options;
501
- opts = {};
499
+ opts = new NullObject();
502
500
  }
503
501
 
504
502
  // support optional filename, where options may be in it's place
@@ -519,7 +517,7 @@ module.exports = class Response extends Writable {
519
517
  }
520
518
  set(field, value) {
521
519
  if(this.headersSent) {
522
- throw new Error('Can\'t write headers: Response was already sent');
520
+ throw new Error('Cannot set headers after they are sent to the client');
523
521
  }
524
522
  if(typeof field === 'object') {
525
523
  for(const header in field) {
@@ -578,10 +576,10 @@ module.exports = class Response extends Writable {
578
576
  render(view, options, callback) {
579
577
  if(typeof options === 'function') {
580
578
  callback = options;
581
- options = {};
579
+ options = new NullObject();
582
580
  }
583
581
  if(!options) {
584
- options = {};
582
+ options = new NullObject();
585
583
  } else {
586
584
  options = Object.assign({}, options);
587
585
  }
@@ -595,7 +593,7 @@ module.exports = class Response extends Writable {
595
593
  }
596
594
  cookie(name, value, options) {
597
595
  if(!options) {
598
- options = {};
596
+ options = new NullObject();
599
597
  }
600
598
  let val = typeof value === 'object' ? "j:"+JSON.stringify(value) : String(value);
601
599
  if(options.maxAge != null) {
@@ -708,9 +706,7 @@ module.exports = class Response extends Writable {
708
706
  let ct = type.indexOf('/') === -1
709
707
  ? (mime.contentType(type) || 'application/octet-stream')
710
708
  : type;
711
- if(ct.startsWith('text/') || ct === 'application/json' || ct === 'application/javascript') {
712
- ct += '; charset=UTF-8';
713
- }
709
+
714
710
  return this.set('content-type', ct);
715
711
  }
716
712
  contentType(type) {
@@ -784,4 +780,4 @@ function pipeStreamOverResponse(res, readStream, totalSize, callback) {
784
780
  if(res.socketExists) res.socket.emit('error', e);
785
781
  }
786
782
  });
787
- }
783
+ }
package/src/router.js CHANGED
@@ -18,6 +18,7 @@ const { patternToRegex, needsConversionToRegex, deprecated, findIndexStartingFro
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
22
 
22
23
  let routeKey = 0;
23
24
 
@@ -268,7 +269,7 @@ module.exports = class Router extends EventEmitter {
268
269
  const fn = async (res, req) => {
269
270
  const { request, response } = this.handleRequest(res, req);
270
271
  if(route.optimizedParams) {
271
- request.optimizedParams = {};
272
+ request.optimizedParams = new NullObject();
272
273
  for(let i = 0; i < route.optimizedParams.length; i++) {
273
274
  request.optimizedParams[route.optimizedParams[i]] = req.getParameter(i);
274
275
  }
@@ -319,7 +320,7 @@ module.exports = class Router extends EventEmitter {
319
320
 
320
321
  _extractParams(pattern, path) {
321
322
  let match = pattern.exec(path);
322
- const obj = match?.groups ?? {};
323
+ const obj = match?.groups ?? new NullObject();
323
324
  for(let i = 1; i < match.length; i++) {
324
325
  obj[i - 1] = match[i];
325
326
  }
@@ -342,7 +343,7 @@ module.exports = class Router extends EventEmitter {
342
343
  }
343
344
  }
344
345
  } else {
345
- req.params = {};
346
+ req.params = new NullObject();
346
347
  if(req._paramStack.length > 0) {
347
348
  for(let params of req._paramStack) {
348
349
  req.params = {...params, ...req.params};
@@ -485,7 +486,12 @@ module.exports = class Router extends EventEmitter {
485
486
  const out = callback(req, res, next);
486
487
  if(out instanceof Promise) {
487
488
  out.catch(err => {
488
- throw err;
489
+ if(this.get("catch async errors")) {
490
+ this._handleError(err, req, res);
491
+ return resolve(true);
492
+ } else {
493
+ throw err;
494
+ }
489
495
  });
490
496
  }
491
497
  } catch(err) {
@@ -528,7 +534,7 @@ module.exports = class Router extends EventEmitter {
528
534
  }
529
535
 
530
536
  route(path) {
531
- let fns = {};
537
+ let fns = new NullObject();
532
538
  for(let method of methods) {
533
539
  fns[method] = (...callbacks) => {
534
540
  return this.createRoute(method.toUpperCase(), path, fns, ...callbacks);
package/src/types.d.ts ADDED
@@ -0,0 +1,49 @@
1
+ declare module "ultimate-express" {
2
+ import e from "@types/express";
3
+ import { AppOptions } from "uWebSockets.js";
4
+
5
+ type Settings = {
6
+ uwsOptions?: AppOptions;
7
+ threads?: number;
8
+ };
9
+
10
+ namespace express {
11
+ export import json = e.json;
12
+ export import raw = e.raw;
13
+ export import text = e.text;
14
+
15
+ // export import application = e.application;
16
+ export import request = e.request;
17
+ export import response = e.response;
18
+
19
+ export import static = e.static;
20
+ // export import query = e.query;
21
+
22
+ export import urlencoded = e.urlencoded;
23
+
24
+ export import RouterOptions = e.RouterOptions;
25
+ export import Application = e.Application;
26
+ export import CookieOptions = e.CookieOptions;
27
+ export import Errback = e.Errback;
28
+ export import ErrorRequestHandler = e.ErrorRequestHandler;
29
+ export import Express = e.Express;
30
+ export import Handler = e.Handler;
31
+ export import IRoute = e.IRoute;
32
+ export import IRouter = e.IRouter;
33
+ export import IRouterHandler = e.IRouterHandler;
34
+ export import IRouterMatcher = e.IRouterMatcher;
35
+ export import MediaType = e.MediaType;
36
+ export import NextFunction = e.NextFunction;
37
+ export import Locals = e.Locals;
38
+ export import Request = e.Request;
39
+ export import RequestHandler = e.RequestHandler;
40
+ export import RequestParamHandler = e.RequestParamHandler;
41
+ export import Response = e.Response;
42
+ export import Router = e.Router;
43
+ export import Send = e.Send;
44
+ }
45
+
46
+ function express(settings?: Settings): e.Express;
47
+
48
+ export = express;
49
+ }
package/src/utils.js CHANGED
@@ -315,6 +315,10 @@ function isRangeFresh(req, res) {
315
315
  return parseHttpDate(lastModified) <= parseHttpDate(ifRange);
316
316
  }
317
317
 
318
+ // fast null object
319
+ const NullObject = function() {};
320
+ NullObject.prototype = Object.create(null);
321
+
318
322
  module.exports = {
319
323
  removeDuplicateSlashes,
320
324
  patternToRegex,
@@ -326,6 +330,7 @@ module.exports = {
326
330
  compileTrust,
327
331
  deprecated,
328
332
  UP_PATH_REGEXP,
333
+ NullObject,
329
334
  decode,
330
335
  containsDotFile,
331
336
  parseTokenList,
package/src/view.js CHANGED
@@ -16,11 +16,12 @@ limitations under the License.
16
16
 
17
17
  const path = require("path");
18
18
  const fs = require("fs");
19
+ const { NullObject } = require("./utils.js");
19
20
 
20
21
  module.exports = class View {
21
22
  constructor(name, options) {
22
23
  this.name = name;
23
- this.options = options ? Object.assign({}, options) : {};
24
+ this.options = options ? Object.assign({}, options) : new NullObject();
24
25
  this.defaultEngine = options.defaultEngine;
25
26
  this.ext = path.extname(name);
26
27
  this.root = options.root;