ultimate-express 1.4.9 → 2.0.0
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 -1
- package/package.json +12 -13
- package/src/declarative.js +27 -3
- package/src/middlewares.js +5 -1
- package/src/response.js +47 -10
- package/src/router.js +6 -2
package/README.md
CHANGED
|
@@ -8,10 +8,12 @@ To make sure µExpress matches behavior of Express in all cases, we run all test
|
|
|
8
8
|
|
|
9
9
|
`npm install ultimate-express` -> replace `express` with `ultimate-express` -> done[*](https://github.com/dimdenGD/ultimate-express?tab=readme-ov-file#differences-from-express)
|
|
10
10
|
|
|
11
|
-
[](https://nodejs.org)
|
|
12
12
|
[](https://npmjs.com/package/ultimate-express)
|
|
13
13
|
[](https://patreon.com/dimdendev)
|
|
14
14
|
|
|
15
|
+
> Use `npm install ultimate-express@node-v18` to install last version that supported Node.js v18.
|
|
16
|
+
|
|
15
17
|
## Difference from similar projects
|
|
16
18
|
|
|
17
19
|
Similar projects based on uWebSockets:
|
|
@@ -35,6 +37,7 @@ Tested using [wrk](https://github.com/wg/wrk) (`-d 60 -t 1 -c 200`). Tested on U
|
|
|
35
37
|
| middlewares/express-static (/static/index.js) | 6.58k | 32.45k | 10.15 MB/sec | 49.43 MB/sec | **4.87X** |
|
|
36
38
|
| engines/ejs (/test) | 5.50k | 40.82k | 2.45 MB/sec | 18.38 MB/sec | **7.42X** |
|
|
37
39
|
| middlewares/body-urlencoded (/abc) | 8.07k | 50.52k | 1.68 MB/sec | 10.78 MB/sec | **6.26X** |
|
|
40
|
+
| middlewares/compression-file (/small-file) | 4.81k | 14.92k | 386 MB/sec | 1.17 GB/sec | **3.10X** |
|
|
38
41
|
|
|
39
42
|
### Performance against other frameworks
|
|
40
43
|
|
|
@@ -327,6 +330,8 @@ Almost all middlewares that are compatible with Express are compatible with µEx
|
|
|
327
330
|
- ✅ [passport](https://www.npmjs.com/package/passport)
|
|
328
331
|
- ✅ [morgan](https://www.npmjs.com/package/morgan)
|
|
329
332
|
- ✅ [swagger-ui-express](https://www.npmjs.com/package/swagger-ui-express)
|
|
333
|
+
- ✅ [graphql-http](https://www.npmjs.com/package/graphql-http)
|
|
334
|
+
- ✅ [better-sse](https://www.npmjs.com/package/better-sse)
|
|
330
335
|
|
|
331
336
|
Middlewares and modules that are confirmed to not work:
|
|
332
337
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultimate-express",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
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": {
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"cover:report": "nyc report --reporter=html"
|
|
12
12
|
},
|
|
13
13
|
"engines": {
|
|
14
|
-
"node": ">=
|
|
14
|
+
"node": ">=20"
|
|
15
15
|
},
|
|
16
16
|
"files": [
|
|
17
17
|
"src",
|
|
@@ -61,48 +61,47 @@
|
|
|
61
61
|
"range-parser": "^1.2.1",
|
|
62
62
|
"statuses": "^2.0.1",
|
|
63
63
|
"tseep": "^1.3.1",
|
|
64
|
-
"type-is": "^
|
|
65
|
-
"uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.
|
|
64
|
+
"type-is": "^2.0.1",
|
|
65
|
+
"uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.52.0",
|
|
66
66
|
"vary": "^1.1.2"
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
69
69
|
"@codechecks/client": "^0.1.12",
|
|
70
70
|
"better-sse": "^0.14.1",
|
|
71
|
-
"body-parser": "^
|
|
71
|
+
"body-parser": "^2.2.0",
|
|
72
72
|
"compression": "^1.8.0",
|
|
73
|
-
"cookie-parser": "^1.4.
|
|
73
|
+
"cookie-parser": "^1.4.7",
|
|
74
74
|
"cookie-session": "^2.1.0",
|
|
75
75
|
"cors": "^2.8.5",
|
|
76
76
|
"ejs": "^3.1.10",
|
|
77
77
|
"errorhandler": "^1.5.1",
|
|
78
|
-
"eventsource": "^3.0.
|
|
78
|
+
"eventsource": "^3.0.7",
|
|
79
79
|
"exit-hook": "^2.2.1",
|
|
80
80
|
"express": "latest-4",
|
|
81
81
|
"express-art-template": "^1.0.1",
|
|
82
82
|
"express-async-errors": "^3.1.1",
|
|
83
83
|
"express-dot-engine": "^1.0.8",
|
|
84
84
|
"express-fileupload": "^1.5.1",
|
|
85
|
-
"express-handlebars": "^8.0.
|
|
85
|
+
"express-handlebars": "^8.0.3",
|
|
86
86
|
"express-mongo-sanitize": "^2.2.0",
|
|
87
87
|
"express-rate-limit": "^7.5.0",
|
|
88
|
-
"express-session": "^1.18.
|
|
88
|
+
"express-session": "^1.18.1",
|
|
89
89
|
"express-subdomain": "^1.0.6",
|
|
90
|
-
"formdata-node": "^6.0.3",
|
|
91
90
|
"graphql-http": "^1.22.4",
|
|
92
91
|
"helmet": "^8.1.0",
|
|
93
92
|
"method-override": "^3.0.0",
|
|
94
93
|
"morgan": "^1.10.0",
|
|
95
|
-
"multer": "^1.4.5-lts.
|
|
94
|
+
"multer": "^1.4.5-lts.2",
|
|
96
95
|
"mustache-express": "^1.3.2",
|
|
97
96
|
"nyc": "^17.1.0",
|
|
98
97
|
"pako": "^2.1.0",
|
|
99
98
|
"passport": "^0.7.0",
|
|
100
99
|
"passport-local": "^1.0.0",
|
|
101
|
-
"pkg-pr-new": "^0.0.
|
|
100
|
+
"pkg-pr-new": "^0.0.43",
|
|
102
101
|
"pug": "^3.0.3",
|
|
103
102
|
"response-time": "^2.3.3",
|
|
104
103
|
"serve-index": "^1.9.1",
|
|
105
|
-
"serve-static": "^
|
|
104
|
+
"serve-static": "^2.2.0",
|
|
106
105
|
"swagger-ui-express": "^5.0.1",
|
|
107
106
|
"swig": "^1.4.2",
|
|
108
107
|
"vhost": "^3.0.2"
|
package/src/declarative.js
CHANGED
|
@@ -31,7 +31,7 @@ module.exports = function compileDeclarative(cb, app) {
|
|
|
31
31
|
|
|
32
32
|
const tokens = [...acorn.tokenizer(code, { ecmaVersion: "latest" })];
|
|
33
33
|
|
|
34
|
-
if(tokens.some(token => ['throw', 'new', 'await'].includes(token.value))) {
|
|
34
|
+
if(tokens.some(token => ['throw', 'new', 'await', 'return'].includes(token.value))) {
|
|
35
35
|
return false;
|
|
36
36
|
}
|
|
37
37
|
|
|
@@ -186,7 +186,7 @@ module.exports = function compileDeclarative(cb, app) {
|
|
|
186
186
|
if(call.arguments[0].type !== 'Literal' || call.arguments[1].type !== 'Literal') {
|
|
187
187
|
return false;
|
|
188
188
|
}
|
|
189
|
-
const sameHeader = headers.find(header => header[0] === call.arguments[0].value);
|
|
189
|
+
const sameHeader = headers.find(header => header[0].toLowerCase() === call.arguments[0].value.toLowerCase());
|
|
190
190
|
if(sameHeader) {
|
|
191
191
|
sameHeader[1] = call.arguments[1].value;
|
|
192
192
|
} else {
|
|
@@ -207,6 +207,12 @@ module.exports = function compileDeclarative(cb, app) {
|
|
|
207
207
|
if(sendUsed) {
|
|
208
208
|
return false;
|
|
209
209
|
}
|
|
210
|
+
if(call.obj.propertyName === 'send') {
|
|
211
|
+
const index = headers.findIndex(header => header[0].toLowerCase() === 'content-type');
|
|
212
|
+
if(index === -1) {
|
|
213
|
+
headers.push(['content-type', 'text/html; charset=utf-8']);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
210
216
|
const arg = call.arguments[0];
|
|
211
217
|
if(arg) {
|
|
212
218
|
if(arg.type === 'Literal') {
|
|
@@ -216,6 +222,17 @@ module.exports = function compileDeclarative(cb, app) {
|
|
|
216
222
|
let val = arg.value;
|
|
217
223
|
if(val === null) {
|
|
218
224
|
val = '';
|
|
225
|
+
const index = headers.findIndex(header => header[0].toLowerCase() === 'content-type');
|
|
226
|
+
if(index !== -1) {
|
|
227
|
+
headers.splice(index, 1);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if(typeof val === 'boolean') {
|
|
231
|
+
if(!headers.some(header => header[0].toLowerCase() === 'content-type')) {
|
|
232
|
+
headers.push(['content-type', 'application/json; charset=utf-8']);
|
|
233
|
+
} else {
|
|
234
|
+
headers.find(header => header[0].toLowerCase() === 'content-type')[1] = 'application/json; charset=utf-8';
|
|
235
|
+
}
|
|
219
236
|
}
|
|
220
237
|
body.push({type: 'text', value: val});
|
|
221
238
|
} else if(arg.type === 'TemplateLiteral') {
|
|
@@ -304,7 +321,11 @@ module.exports = function compileDeclarative(cb, app) {
|
|
|
304
321
|
return false;
|
|
305
322
|
}
|
|
306
323
|
|
|
307
|
-
headers.
|
|
324
|
+
if(!headers.some(header => header[0].toLowerCase() === 'content-type')) {
|
|
325
|
+
headers.push(['content-type', 'application/json; charset=utf-8']);
|
|
326
|
+
} else {
|
|
327
|
+
headers.find(header => header[0].toLowerCase() === 'content-type')[1] = 'application/json; charset=utf-8';
|
|
328
|
+
}
|
|
308
329
|
body.push({
|
|
309
330
|
type: 'text',
|
|
310
331
|
value:
|
|
@@ -334,6 +355,9 @@ module.exports = function compileDeclarative(cb, app) {
|
|
|
334
355
|
if(header[0].toLowerCase() === 'content-length') {
|
|
335
356
|
return false;
|
|
336
357
|
}
|
|
358
|
+
if(header[0].toLowerCase() === 'content-type' && header[1].includes('text/') && !header[1].includes(';')) {
|
|
359
|
+
header[1] += '; charset=utf-8';
|
|
360
|
+
}
|
|
337
361
|
decRes = decRes.writeHeader(header[0], header[1]);
|
|
338
362
|
}
|
|
339
363
|
|
package/src/middlewares.js
CHANGED
|
@@ -169,7 +169,7 @@ function createBodyParser(defaultType, beforeReturn) {
|
|
|
169
169
|
|
|
170
170
|
req.body = new NullObject();
|
|
171
171
|
|
|
172
|
-
// skip reading body for
|
|
172
|
+
// skip reading body for no content type
|
|
173
173
|
if(!type) {
|
|
174
174
|
return next();
|
|
175
175
|
}
|
|
@@ -281,6 +281,10 @@ const json = createBodyParser('application/json', function(req, res, next, optio
|
|
|
281
281
|
return next(new Error('Invalid body'));
|
|
282
282
|
}
|
|
283
283
|
}
|
|
284
|
+
if(buf.length === 0) {
|
|
285
|
+
req.body = {};
|
|
286
|
+
return next();
|
|
287
|
+
}
|
|
284
288
|
try {
|
|
285
289
|
req.body = JSON.parse(buf.toString(), options.reviver);
|
|
286
290
|
} catch(e) {
|
package/src/response.js
CHANGED
|
@@ -177,6 +177,7 @@ module.exports = class Response extends Writable {
|
|
|
177
177
|
}
|
|
178
178
|
});
|
|
179
179
|
}, 50);
|
|
180
|
+
this.#writeTimeout.unref();
|
|
180
181
|
}
|
|
181
182
|
this.writingChunk = false;
|
|
182
183
|
callback(null);
|
|
@@ -243,9 +244,6 @@ module.exports = class Response extends Writable {
|
|
|
243
244
|
this._res.writeHeader(header, value);
|
|
244
245
|
}
|
|
245
246
|
}
|
|
246
|
-
if(!this.headers['content-type']) {
|
|
247
|
-
this._res.writeHeader('content-type', 'text/html' + (utf8 ? `; charset=utf-8` : ''));
|
|
248
|
-
}
|
|
249
247
|
this.headersSent = true;
|
|
250
248
|
}
|
|
251
249
|
_implicitHeader() {
|
|
@@ -285,6 +283,8 @@ module.exports = class Response extends Writable {
|
|
|
285
283
|
this._res.end();
|
|
286
284
|
this.finished = true;
|
|
287
285
|
if(this.socketExists) this.socket.emit('close');
|
|
286
|
+
this.emit('finish');
|
|
287
|
+
this.emit('close');
|
|
288
288
|
return;
|
|
289
289
|
}
|
|
290
290
|
}
|
|
@@ -323,6 +323,7 @@ module.exports = class Response extends Writable {
|
|
|
323
323
|
const isBuffer = Buffer.isBuffer(body);
|
|
324
324
|
if(body === null || body === undefined) {
|
|
325
325
|
body = '';
|
|
326
|
+
return this.end(body);
|
|
326
327
|
} else if(typeof body === 'object' && !isBuffer) {
|
|
327
328
|
return this.json(body);
|
|
328
329
|
} else if(typeof body === 'number') {
|
|
@@ -331,18 +332,25 @@ module.exports = class Response extends Writable {
|
|
|
331
332
|
return this.status(body).send(arguments[1]);
|
|
332
333
|
} else {
|
|
333
334
|
deprecated('res.send(status)', 'res.sendStatus(status)');
|
|
335
|
+
if(!this.headers['content-type']) {
|
|
336
|
+
this.headers['content-type'] = 'text/plain; charset=utf-8';
|
|
337
|
+
}
|
|
334
338
|
return this.sendStatus(body);
|
|
335
339
|
}
|
|
340
|
+
} else if(typeof body === 'boolean') {
|
|
341
|
+
return this.json(body);
|
|
336
342
|
} else if(!isBuffer) {
|
|
337
343
|
body = String(body);
|
|
338
344
|
}
|
|
339
|
-
if(typeof body === 'string') {
|
|
345
|
+
if(typeof body === 'string' && !isBuffer) {
|
|
340
346
|
const contentType = this.headers['content-type'];
|
|
341
347
|
if(!contentType) {
|
|
342
348
|
this.headers['content-type'] = 'text/html; charset=utf-8';
|
|
343
349
|
} else if(!contentType.includes(';')) {
|
|
344
350
|
this.headers['content-type'] += '; charset=utf-8';
|
|
345
351
|
}
|
|
352
|
+
} else {
|
|
353
|
+
this.headers['content-type'] = 'application/octet-stream';
|
|
346
354
|
}
|
|
347
355
|
return this.end(body);
|
|
348
356
|
}
|
|
@@ -611,6 +619,9 @@ module.exports = class Response extends Writable {
|
|
|
611
619
|
getHeader(field) {
|
|
612
620
|
return this.get(field);
|
|
613
621
|
}
|
|
622
|
+
getHeaders(){
|
|
623
|
+
return this.headers;
|
|
624
|
+
}
|
|
614
625
|
removeHeader(field) {
|
|
615
626
|
delete this.headers[field.toLowerCase()];
|
|
616
627
|
return this;
|
|
@@ -714,11 +725,7 @@ module.exports = class Response extends Writable {
|
|
|
714
725
|
jsonp(object) {
|
|
715
726
|
let callback = this.req.query[this.app.get('jsonp callback name')];
|
|
716
727
|
let body = stringify(object, this.app.get('json replacer'), this.app.get('json spaces'), this.app.get('json escape'));
|
|
717
|
-
|
|
718
|
-
if(!this.headers['content-type']) {
|
|
719
|
-
this.headers['content-type'] = 'application/javascript; charset=utf-8';
|
|
720
|
-
this.headers['X-Content-Type-Options'] = 'nosniff';
|
|
721
|
-
}
|
|
728
|
+
let js = false;
|
|
722
729
|
|
|
723
730
|
if(Array.isArray(callback)) {
|
|
724
731
|
callback = callback[0];
|
|
@@ -736,6 +743,13 @@ module.exports = class Response extends Writable {
|
|
|
736
743
|
.replace(/\u2029/g, '\\u2029')
|
|
737
744
|
}
|
|
738
745
|
body = '/**/ typeof ' + callback + ' === \'function\' && ' + callback + '(' + body + ');';
|
|
746
|
+
js = true;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
if(!this.headers['content-type']) {
|
|
751
|
+
this.headers['content-type'] = `${js ? 'text/javascript' : 'application/json'}; charset=utf-8`;
|
|
752
|
+
if(js) this.headers['X-Content-Type-Options'] = 'nosniff';
|
|
739
753
|
}
|
|
740
754
|
|
|
741
755
|
return this.send(body);
|
|
@@ -760,7 +774,25 @@ module.exports = class Response extends Writable {
|
|
|
760
774
|
this.location(url);
|
|
761
775
|
this.status(status);
|
|
762
776
|
this.headers['content-type'] = 'text/plain; charset=utf-8';
|
|
763
|
-
|
|
777
|
+
let body;
|
|
778
|
+
// Support text/{plain,html} by default
|
|
779
|
+
this.format({
|
|
780
|
+
text: function() {
|
|
781
|
+
body = statuses.message[status] + '. Redirecting to ' + url
|
|
782
|
+
},
|
|
783
|
+
html: function() {
|
|
784
|
+
let u = escapeHtml(url);
|
|
785
|
+
body = '<p>' + statuses.message[status] + '. Redirecting to ' + u + '</p>'
|
|
786
|
+
},
|
|
787
|
+
default: function() {
|
|
788
|
+
body = '';
|
|
789
|
+
}
|
|
790
|
+
});
|
|
791
|
+
if (this.req.method === 'HEAD') {
|
|
792
|
+
this.end();
|
|
793
|
+
} else {
|
|
794
|
+
this.end(body);
|
|
795
|
+
}
|
|
764
796
|
}
|
|
765
797
|
|
|
766
798
|
type(type) {
|
|
@@ -773,6 +805,11 @@ module.exports = class Response extends Writable {
|
|
|
773
805
|
contentType = this.type;
|
|
774
806
|
|
|
775
807
|
vary(field) {
|
|
808
|
+
// checks for back-compat
|
|
809
|
+
if (!field || (Array.isArray(field) && !field.length)) {
|
|
810
|
+
deprecate('res.vary(): Provide a field name');
|
|
811
|
+
return this;
|
|
812
|
+
}
|
|
776
813
|
vary(this, field);
|
|
777
814
|
return this;
|
|
778
815
|
}
|
package/src/router.js
CHANGED
|
@@ -180,7 +180,7 @@ module.exports = class Router extends EventEmitter {
|
|
|
180
180
|
optimizedPathToRouter = optimizedPathToRouter.slice(0, -1); // remove last element, which is the router itself
|
|
181
181
|
if(optimizedPathToRouter) {
|
|
182
182
|
// wait for routes in router to be registered
|
|
183
|
-
setTimeout(() => {
|
|
183
|
+
const t = setTimeout(() => {
|
|
184
184
|
if(!this.listenCalled) {
|
|
185
185
|
return; // can only optimize router whos parent is listening
|
|
186
186
|
}
|
|
@@ -208,6 +208,7 @@ module.exports = class Router extends EventEmitter {
|
|
|
208
208
|
}
|
|
209
209
|
}
|
|
210
210
|
}, 100);
|
|
211
|
+
t.unref();
|
|
211
212
|
}
|
|
212
213
|
// only 1 router can be optimized per route
|
|
213
214
|
break;
|
|
@@ -354,7 +355,10 @@ module.exports = class Router extends EventEmitter {
|
|
|
354
355
|
path = path.slice(0, -1);
|
|
355
356
|
}
|
|
356
357
|
let match = pattern.exec(path);
|
|
357
|
-
|
|
358
|
+
if( match?.groups ){
|
|
359
|
+
return match.groups;
|
|
360
|
+
}
|
|
361
|
+
const obj = new NullObject();
|
|
358
362
|
for(let i = 1; i < match.length; i++) {
|
|
359
363
|
obj[i - 1] = match[i];
|
|
360
364
|
}
|