ultimate-express 1.3.1 → 1.3.2
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 +1 -0
- package/package.json +3 -2
- package/src/request.js +24 -23
- package/src/response.js +17 -22
- package/src/router.js +29 -27
- package/src/utils.js +15 -10
package/README.md
CHANGED
|
@@ -302,6 +302,7 @@ Almost all middlewares that are compatible with Express are compatible with µEx
|
|
|
302
302
|
- ✅ [express-rate-limit](https://npmjs.com/package/express-rate-limit)
|
|
303
303
|
- ✅ [express-subdomain](https://npmjs.com/package/express-subdomain)
|
|
304
304
|
- ✅ [vhost](https://npmjs.com/package/vhost)
|
|
305
|
+
- ✅ [tsoa](https://github.com/lukeautry/tsoa)
|
|
305
306
|
|
|
306
307
|
Middlewares and modules that are confirmed to not work:
|
|
307
308
|
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultimate-express",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.2",
|
|
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"
|
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
|
}
|
|
@@ -536,18 +539,12 @@ module.exports = class Response extends Writable {
|
|
|
536
539
|
}
|
|
537
540
|
return this;
|
|
538
541
|
}
|
|
539
|
-
header
|
|
540
|
-
|
|
541
|
-
}
|
|
542
|
-
setHeader(field, value) {
|
|
543
|
-
return this.set(field, value);
|
|
544
|
-
}
|
|
542
|
+
header = this.set;
|
|
543
|
+
setHeader = this.set;
|
|
545
544
|
get(field) {
|
|
546
545
|
return this.headers[field.toLowerCase()];
|
|
547
546
|
}
|
|
548
|
-
getHeader
|
|
549
|
-
return this.get(field);
|
|
550
|
-
}
|
|
547
|
+
getHeader = this.get;
|
|
551
548
|
removeHeader(field) {
|
|
552
549
|
delete this.headers[field.toLowerCase()];
|
|
553
550
|
return this;
|
|
@@ -709,9 +706,7 @@ module.exports = class Response extends Writable {
|
|
|
709
706
|
|
|
710
707
|
return this.set('content-type', ct);
|
|
711
708
|
}
|
|
712
|
-
contentType
|
|
713
|
-
return this.type(type);
|
|
714
|
-
}
|
|
709
|
+
contentType = this.type;
|
|
715
710
|
|
|
716
711
|
vary(field) {
|
|
717
712
|
vary(this, field);
|
package/src/router.js
CHANGED
|
@@ -31,6 +31,8 @@ const methods = [
|
|
|
31
31
|
];
|
|
32
32
|
const supportedUwsMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', 'HEAD', 'CONNECT', 'TRACE'];
|
|
33
33
|
|
|
34
|
+
const regExParam = /:(\w+)/g;
|
|
35
|
+
|
|
34
36
|
module.exports = class Router extends EventEmitter {
|
|
35
37
|
constructor(settings = {}) {
|
|
36
38
|
super();
|
|
@@ -264,7 +266,7 @@ module.exports = class Router extends EventEmitter {
|
|
|
264
266
|
method = 'del';
|
|
265
267
|
}
|
|
266
268
|
if(!route.optimizedRouter && route.path.includes(":")) {
|
|
267
|
-
route.optimizedParams = route.path.match(
|
|
269
|
+
route.optimizedParams = route.path.match(regExParam).map(p => p.slice(1));
|
|
268
270
|
}
|
|
269
271
|
const fn = async (res, req) => {
|
|
270
272
|
const { request, response } = this.handleRequest(res, req);
|
|
@@ -282,7 +284,7 @@ module.exports = class Router extends EventEmitter {
|
|
|
282
284
|
}
|
|
283
285
|
};
|
|
284
286
|
route.optimizedPath = optimizedPath;
|
|
285
|
-
let replacedPath = route.path.replace(
|
|
287
|
+
let replacedPath = route.path.replace(regExParam, ':x');
|
|
286
288
|
this.uwsApp[method](replacedPath, fn);
|
|
287
289
|
if(!this.get('strict routing') && route.path[route.path.length - 1] !== '/') {
|
|
288
290
|
this.uwsApp[method](replacedPath + '/', fn);
|
|
@@ -407,33 +409,33 @@ module.exports = class Router extends EventEmitter {
|
|
|
407
409
|
}
|
|
408
410
|
|
|
409
411
|
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));
|
|
412
|
+
let routeIndex = skipCheck ? startIndex : findIndexStartingFrom(routes, r => (r.all || r.method === req.method || (r.gettable && req.method === 'HEAD')) && this._pathMatches(r, req), startIndex);
|
|
413
|
+
const route = routes[routeIndex];
|
|
414
|
+
if(!route) {
|
|
415
|
+
if(!skipCheck) {
|
|
416
|
+
// on normal unoptimized routes, if theres no match then there is no route
|
|
417
|
+
return false;
|
|
420
418
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
419
|
+
// on optimized routes, there can be more routes, so we have to use unoptimized routing and skip until we find route we stopped at
|
|
420
|
+
return this._routeRequest(req, res, 0, this._routes, false, skipUntil);
|
|
421
|
+
}
|
|
422
|
+
let callbackindex = 0;
|
|
423
|
+
const continueRoute = await this._preprocessRequest(req, res, route);
|
|
424
|
+
if(route.use) {
|
|
425
|
+
const strictRouting = this.get('strict routing');
|
|
426
|
+
req._stack.push(route.path);
|
|
427
|
+
req._opPath = req.path.replace(this.getFullMountpath(req), '');
|
|
428
|
+
if(strictRouting) {
|
|
429
|
+
if(req.endsWithSlash && req.path !== '/') {
|
|
430
|
+
req._opPath += '/';
|
|
433
431
|
}
|
|
434
|
-
|
|
435
|
-
|
|
432
|
+
} else if(req.endsWithSlash && req.path !== '/') {
|
|
433
|
+
req._opPath = req._opPath.slice(0, -1);
|
|
436
434
|
}
|
|
435
|
+
req.url = req._opPath + req.urlQuery;
|
|
436
|
+
if(req.url === '') req.url = '/';
|
|
437
|
+
}
|
|
438
|
+
return new Promise((resolve) => {
|
|
437
439
|
const next = async (thingamabob) => {
|
|
438
440
|
if(thingamabob) {
|
|
439
441
|
if(thingamabob === 'route' || thingamabob === 'skipPop') {
|
|
@@ -565,4 +567,4 @@ module.exports = class Router extends EventEmitter {
|
|
|
565
567
|
`</body>\n` +
|
|
566
568
|
`</html>\n`);
|
|
567
569
|
}
|
|
568
|
-
}
|
|
570
|
+
}
|
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
|
|
|
@@ -218,7 +223,7 @@ function decode (path) {
|
|
|
218
223
|
const UP_PATH_REGEXP = /(?:^|[\\/])\.\.(?:[\\/]|$)/;
|
|
219
224
|
|
|
220
225
|
function containsDotFile(parts) {
|
|
221
|
-
for(let i = 0
|
|
226
|
+
for(let i = 0, len = parts.length; i < len; i++) {
|
|
222
227
|
const part = parts[i];
|
|
223
228
|
if(part.length > 1 && part[0] === '.') {
|
|
224
229
|
return true;
|
|
@@ -341,4 +346,4 @@ module.exports = {
|
|
|
341
346
|
findIndexStartingFrom,
|
|
342
347
|
fastQueryParse,
|
|
343
348
|
canBeOptimized
|
|
344
|
-
};
|
|
349
|
+
};
|