ultimate-express 2.0.1 → 2.0.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 +2 -1
- package/package.json +1 -1
- package/src/application.js +6 -0
- package/src/middlewares.js +3 -2
- package/src/request.js +9 -2
- package/src/response.js +60 -24
- package/src/router.js +23 -2
package/README.md
CHANGED
|
@@ -57,7 +57,7 @@ For full table with other runtimes, check [here](https://github.com/dimdenGD/bun
|
|
|
57
57
|
| express | 10,411.313 | 11,245.57 | 10,598.74 | 9,389.63 |
|
|
58
58
|
|
|
59
59
|
Other benchmarks:
|
|
60
|
-
- [TechEmpower / FrameworkBenchmarks](https://www.techempower.com/benchmarks/#
|
|
60
|
+
- [TechEmpower / FrameworkBenchmarks](https://www.techempower.com/benchmarks/#section=data-r23&test=plaintext&l=zik0sf-pa7)
|
|
61
61
|
- [the-benchmarker / web-frameworks](https://web-frameworks-benchmark.netlify.app/result?l=javascript)
|
|
62
62
|
|
|
63
63
|
### Performance on real-world application
|
|
@@ -189,6 +189,7 @@ In general, basically all features and options are supported. Use [Express 4.x d
|
|
|
189
189
|
- ✅ app.engines
|
|
190
190
|
- ✅ app.on("mount")
|
|
191
191
|
- ✅ HEAD method
|
|
192
|
+
- ✅ OPTIONS method
|
|
192
193
|
|
|
193
194
|
### Application settings
|
|
194
195
|
|
package/package.json
CHANGED
package/src/application.js
CHANGED
|
@@ -191,6 +191,12 @@ class Application extends Router {
|
|
|
191
191
|
if(request._error) {
|
|
192
192
|
return this._handleError(request._error, null, request, response);
|
|
193
193
|
}
|
|
194
|
+
if(request._isOptions && request._matchedMethods.size > 0) {
|
|
195
|
+
const allowedMethods = Array.from(request._matchedMethods).join(',');
|
|
196
|
+
response.setHeader('Allow', allowedMethods);
|
|
197
|
+
response.send(allowedMethods);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
194
200
|
response.status(404);
|
|
195
201
|
this._sendErrorPage(request, response, `Cannot ${request.method} ${request.path}`, false);
|
|
196
202
|
}
|
package/src/middlewares.js
CHANGED
|
@@ -86,8 +86,9 @@ function static(root, options) {
|
|
|
86
86
|
|
|
87
87
|
if(stat.isDirectory()) {
|
|
88
88
|
if(!req.endsWithSlash) {
|
|
89
|
-
if(options.redirect)
|
|
90
|
-
|
|
89
|
+
if(options.redirect) {
|
|
90
|
+
return res.redirect(301, req._originalPath + '/', true);
|
|
91
|
+
} else {
|
|
91
92
|
if(!options.fallthrough) {
|
|
92
93
|
res.status(404);
|
|
93
94
|
return next(new Error('Not found'));
|
package/src/request.js
CHANGED
|
@@ -69,8 +69,11 @@ module.exports = class Request extends Readable {
|
|
|
69
69
|
this._opPath = this._opPath.slice(0, -1);
|
|
70
70
|
}
|
|
71
71
|
this.method = req.getCaseSensitiveMethod().toUpperCase();
|
|
72
|
+
this._isOptions = this.method === 'OPTIONS';
|
|
73
|
+
this._isHead = this.method === 'HEAD';
|
|
72
74
|
this.params = {};
|
|
73
|
-
|
|
75
|
+
|
|
76
|
+
this._matchedMethods = new Set();
|
|
74
77
|
this._gotParams = new Set();
|
|
75
78
|
this._stack = [];
|
|
76
79
|
this._paramStack = [];
|
|
@@ -135,6 +138,10 @@ module.exports = class Request extends Readable {
|
|
|
135
138
|
return match ? match[0] : '';
|
|
136
139
|
}
|
|
137
140
|
|
|
141
|
+
set baseUrl(x) {
|
|
142
|
+
return this._originalPath = x;
|
|
143
|
+
}
|
|
144
|
+
|
|
138
145
|
get #host() {
|
|
139
146
|
const trust = this.app.get('trust proxy fn');
|
|
140
147
|
if(!trust) {
|
|
@@ -248,7 +255,7 @@ module.exports = class Request extends Readable {
|
|
|
248
255
|
if(this.#cachedParsedIp !== null) {
|
|
249
256
|
return this.#cachedParsedIp;
|
|
250
257
|
}
|
|
251
|
-
const finished =
|
|
258
|
+
const finished = this.res.finished;
|
|
252
259
|
if(finished) {
|
|
253
260
|
// mark app as one that needs ip after response
|
|
254
261
|
this.app.needsIpAfterResponse = true;
|
package/src/response.js
CHANGED
|
@@ -69,6 +69,7 @@ class Socket extends EventEmitter {
|
|
|
69
69
|
|
|
70
70
|
module.exports = class Response extends Writable {
|
|
71
71
|
#socket = null;
|
|
72
|
+
#ended = false;
|
|
72
73
|
#pendingChunks = [];
|
|
73
74
|
#lastWriteChunkTime = 0;
|
|
74
75
|
#writeTimeout = null;
|
|
@@ -112,13 +113,16 @@ module.exports = class Response extends Writable {
|
|
|
112
113
|
this._res.cork(() => {
|
|
113
114
|
this._res.close();
|
|
114
115
|
this.finished = true;
|
|
115
|
-
|
|
116
|
+
this.#socket?.emit('close');
|
|
116
117
|
});
|
|
117
118
|
});
|
|
119
|
+
this.once('close', () => {
|
|
120
|
+
this.#ended = true
|
|
121
|
+
})
|
|
118
122
|
}
|
|
119
123
|
|
|
120
124
|
get socket() {
|
|
121
|
-
this
|
|
125
|
+
if(this.#ended) return null;
|
|
122
126
|
if(!this.#socket) {
|
|
123
127
|
this.#socket = new Socket(this);
|
|
124
128
|
}
|
|
@@ -153,7 +157,7 @@ module.exports = class Response extends Writable {
|
|
|
153
157
|
if (this.chunkedTransfer) {
|
|
154
158
|
this.#pendingChunks.push(chunk);
|
|
155
159
|
const size = this.#pendingChunks.reduce((acc, chunk) => acc + chunk.byteLength, 0);
|
|
156
|
-
const now =
|
|
160
|
+
const now = performance.now();
|
|
157
161
|
// the first chunk is sent immediately (!this.#lastWriteChunkTime)
|
|
158
162
|
// the other chunks are sent when watermark is reached (size >= HIGH_WATERMARK)
|
|
159
163
|
// or if elapsed 50ms of last send (now - this.#lastWriteChunkTime > 50)
|
|
@@ -173,7 +177,7 @@ module.exports = class Response extends Writable {
|
|
|
173
177
|
const size = this.#pendingChunks.reduce((acc, chunk) => acc + chunk.byteLength, 0);
|
|
174
178
|
this._res.write(Buffer.concat(this.#pendingChunks, size));
|
|
175
179
|
this.#pendingChunks = [];
|
|
176
|
-
this.#lastWriteChunkTime = now;
|
|
180
|
+
this.#lastWriteChunkTime = performance.now();
|
|
177
181
|
}
|
|
178
182
|
});
|
|
179
183
|
}, 50);
|
|
@@ -188,7 +192,7 @@ module.exports = class Response extends Writable {
|
|
|
188
192
|
super.end();
|
|
189
193
|
this.finished = true;
|
|
190
194
|
this.writingChunk = false;
|
|
191
|
-
|
|
195
|
+
this.#socket?.emit('close');
|
|
192
196
|
callback(null);
|
|
193
197
|
} else if (!ok) {
|
|
194
198
|
this._res.ab = chunk;
|
|
@@ -198,7 +202,7 @@ module.exports = class Response extends Writable {
|
|
|
198
202
|
const [ok, done] = this._res.tryEnd(this._res.ab.slice(offset - this._res.abOffset), this.totalSize);
|
|
199
203
|
if (done) {
|
|
200
204
|
this.finished = true;
|
|
201
|
-
|
|
205
|
+
this.#socket?.emit('close');
|
|
202
206
|
}
|
|
203
207
|
if (ok) {
|
|
204
208
|
this.writingChunk = false;
|
|
@@ -258,10 +262,18 @@ module.exports = class Response extends Writable {
|
|
|
258
262
|
sendStatus(code) {
|
|
259
263
|
return this.status(code).send(statuses.message[+code] ?? code.toString());
|
|
260
264
|
}
|
|
261
|
-
end(data) {
|
|
265
|
+
end(data, cb) {
|
|
266
|
+
if(typeof data === 'function') {
|
|
267
|
+
cb = data;
|
|
268
|
+
data = undefined;
|
|
269
|
+
}
|
|
270
|
+
if(typeof cb !== 'function') {
|
|
271
|
+
cb = undefined; // silence the error?
|
|
272
|
+
}
|
|
273
|
+
|
|
262
274
|
if(this.writingChunk) {
|
|
263
275
|
this.once('drain', () => {
|
|
264
|
-
this.end(data);
|
|
276
|
+
this.end(data, cb);
|
|
265
277
|
});
|
|
266
278
|
return;
|
|
267
279
|
}
|
|
@@ -282,9 +294,13 @@ module.exports = class Response extends Writable {
|
|
|
282
294
|
if(fresh) {
|
|
283
295
|
this._res.end();
|
|
284
296
|
this.finished = true;
|
|
285
|
-
|
|
297
|
+
this.#socket?.emit('close');
|
|
286
298
|
this.emit('finish');
|
|
287
299
|
this.emit('close');
|
|
300
|
+
cb && queueMicrotask(() => {
|
|
301
|
+
this.#ended = true;
|
|
302
|
+
cb();
|
|
303
|
+
});
|
|
288
304
|
return;
|
|
289
305
|
}
|
|
290
306
|
}
|
|
@@ -309,9 +325,13 @@ module.exports = class Response extends Writable {
|
|
|
309
325
|
}
|
|
310
326
|
|
|
311
327
|
this.finished = true;
|
|
312
|
-
|
|
328
|
+
this.#socket?.emit('close');
|
|
313
329
|
this.emit('finish');
|
|
314
330
|
this.emit('close');
|
|
331
|
+
cb && queueMicrotask(() => {
|
|
332
|
+
this.#ended = true;
|
|
333
|
+
cb();
|
|
334
|
+
});
|
|
315
335
|
});
|
|
316
336
|
return this;
|
|
317
337
|
}
|
|
@@ -775,28 +795,44 @@ module.exports = class Response extends Writable {
|
|
|
775
795
|
}
|
|
776
796
|
return this.headers['location'] = encodeUrl(path);
|
|
777
797
|
}
|
|
778
|
-
redirect(status, url) {
|
|
798
|
+
redirect(status, url, forceHtml = false) {
|
|
779
799
|
if(typeof status !== 'number' && !url) {
|
|
780
800
|
url = status;
|
|
781
801
|
status = 302;
|
|
782
802
|
}
|
|
783
803
|
this.location(url);
|
|
784
804
|
this.status(status);
|
|
785
|
-
this.headers['content-type'] = 'text/plain; charset=utf-8';
|
|
786
805
|
let body;
|
|
787
806
|
// Support text/{plain,html} by default
|
|
788
|
-
|
|
789
|
-
text
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
body
|
|
798
|
-
|
|
799
|
-
|
|
807
|
+
if(forceHtml) {
|
|
808
|
+
this.set('Content-Type', 'text/html; charset=UTF-8');
|
|
809
|
+
body =
|
|
810
|
+
'<!DOCTYPE html>\n' +
|
|
811
|
+
'<html lang="en">\n' +
|
|
812
|
+
'<head>\n' +
|
|
813
|
+
'<meta charset="utf-8">\n' +
|
|
814
|
+
'<title>Redirecting</title>\n' +
|
|
815
|
+
'</head>\n' +
|
|
816
|
+
'<body>\n' +
|
|
817
|
+
`<pre>Redirecting to ${url.replaceAll("<", "<").replaceAll(">", ">")}</pre>\n` +
|
|
818
|
+
'</body>\n' +
|
|
819
|
+
'</html>\n';
|
|
820
|
+
} else {
|
|
821
|
+
this.format({
|
|
822
|
+
text: () => {
|
|
823
|
+
this.set('Content-Type', 'text/plain; charset=UTF-8');
|
|
824
|
+
body = statuses.message[status] + '. Redirecting to ' + url
|
|
825
|
+
},
|
|
826
|
+
html: () => {
|
|
827
|
+
this.set('Content-Type', 'text/html; charset=UTF-8');
|
|
828
|
+
body = `<p>${statuses.message[status]}. Redirecting to ${url.replaceAll("<", "<").replaceAll(">", ">")}</p>`;
|
|
829
|
+
},
|
|
830
|
+
default: () => {
|
|
831
|
+
this.set('Content-Type', 'text/plain; charset=UTF-8');
|
|
832
|
+
body = '';
|
|
833
|
+
}
|
|
834
|
+
});
|
|
835
|
+
}
|
|
800
836
|
if (this.req.method === 'HEAD') {
|
|
801
837
|
this.end();
|
|
802
838
|
} else {
|
package/src/router.js
CHANGED
|
@@ -152,6 +152,9 @@ module.exports = class Router extends EventEmitter {
|
|
|
152
152
|
all: method === 'ALL' || method === 'USE',
|
|
153
153
|
gettable: method === 'GET' || method === 'HEAD',
|
|
154
154
|
};
|
|
155
|
+
if(typeof route.path === 'string' && (route.path.includes(':') || route.path.includes('*') || (route.path.includes('(') && route.path.includes(')'))) && route.pattern instanceof RegExp) {
|
|
156
|
+
route.complex = true;
|
|
157
|
+
}
|
|
155
158
|
routes.push(route);
|
|
156
159
|
// normal routes optimization
|
|
157
160
|
if(canBeOptimized(route.path) && route.pattern !== '/*' && !this.parent && this.get('case sensitive routing') && this.uwsApp) {
|
|
@@ -296,6 +299,12 @@ module.exports = class Router extends EventEmitter {
|
|
|
296
299
|
if(request._error) {
|
|
297
300
|
return this._handleError(request._error, null, request, response);
|
|
298
301
|
}
|
|
302
|
+
if(request._isOptions && request._matchedMethods.size > 0) {
|
|
303
|
+
const allowedMethods = Array.from(request._matchedMethods).join(',');
|
|
304
|
+
response.setHeader('Allow', allowedMethods);
|
|
305
|
+
response.send(allowedMethods);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
299
308
|
response.status(404);
|
|
300
309
|
request.noEtag = true;
|
|
301
310
|
this._sendErrorPage(request, response, `Cannot ${request.method} ${request._originalPath}`, false);
|
|
@@ -369,7 +378,7 @@ module.exports = class Router extends EventEmitter {
|
|
|
369
378
|
req.route = route;
|
|
370
379
|
if(route.optimizedParams) {
|
|
371
380
|
req.params = {...req.optimizedParams};
|
|
372
|
-
} else if(
|
|
381
|
+
} else if(route.complex) {
|
|
373
382
|
let path = req._originalPath;
|
|
374
383
|
if(req._stack.length > 0) {
|
|
375
384
|
path = path.replace(this.getFullMountpath(req), '');
|
|
@@ -445,7 +454,7 @@ module.exports = class Router extends EventEmitter {
|
|
|
445
454
|
}
|
|
446
455
|
|
|
447
456
|
async _routeRequest(req, res, startIndex = 0, routes = this._routes, skipCheck = false, skipUntil) {
|
|
448
|
-
let routeIndex = skipCheck ? startIndex : findIndexStartingFrom(routes, r => (r.all || r.method === req.method || (r.gettable && req.
|
|
457
|
+
let routeIndex = skipCheck ? startIndex : findIndexStartingFrom(routes, r => (r.all || r.method === req.method || req._isOptions || (r.gettable && req._isHead)) && this._pathMatches(r, req), startIndex);
|
|
449
458
|
const route = routes[routeIndex];
|
|
450
459
|
if(!route) {
|
|
451
460
|
if(!skipCheck) {
|
|
@@ -487,6 +496,9 @@ module.exports = class Router extends EventEmitter {
|
|
|
487
496
|
if(thingamabob) {
|
|
488
497
|
if(thingamabob === 'route' || thingamabob === 'skipPop') {
|
|
489
498
|
if(route.use && thingamabob !== 'skipPop') {
|
|
499
|
+
if(req._isOptions) {
|
|
500
|
+
return resolve(false);
|
|
501
|
+
}
|
|
490
502
|
req._stack.pop();
|
|
491
503
|
|
|
492
504
|
req._opPath = req._stack.length > 0 ? req._originalPath.replace(this.getFullMountpath(req), '') : req._originalPath;
|
|
@@ -546,6 +558,15 @@ module.exports = class Router extends EventEmitter {
|
|
|
546
558
|
}
|
|
547
559
|
|
|
548
560
|
try {
|
|
561
|
+
// handling OPTIONS method
|
|
562
|
+
if(req._isOptions && !route.all && route.method !== 'OPTIONS') {
|
|
563
|
+
req._matchedMethods.add(route.method);
|
|
564
|
+
if(route.gettable) {
|
|
565
|
+
req._matchedMethods.add('HEAD');
|
|
566
|
+
}
|
|
567
|
+
return next();
|
|
568
|
+
}
|
|
569
|
+
|
|
549
570
|
// skipping routes we already went through via optimized path
|
|
550
571
|
if(!skipCheck && skipUntil && skipUntil.routeKey >= route.routeKey) {
|
|
551
572
|
return next();
|