ultimate-express 1.4.8 → 1.4.10
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 +4 -1
- package/package.json +4 -1
- package/src/declarative.js +32 -3
- package/src/middlewares.js +5 -1
- package/src/response.js +69 -14
- package/src/router.js +9 -2
- package/src/view.js +12 -13
package/README.md
CHANGED
|
@@ -35,6 +35,7 @@ Tested using [wrk](https://github.com/wg/wrk) (`-d 60 -t 1 -c 200`). Tested on U
|
|
|
35
35
|
| middlewares/express-static (/static/index.js) | 6.58k | 32.45k | 10.15 MB/sec | 49.43 MB/sec | **4.87X** |
|
|
36
36
|
| engines/ejs (/test) | 5.50k | 40.82k | 2.45 MB/sec | 18.38 MB/sec | **7.42X** |
|
|
37
37
|
| middlewares/body-urlencoded (/abc) | 8.07k | 50.52k | 1.68 MB/sec | 10.78 MB/sec | **6.26X** |
|
|
38
|
+
| middlewares/compression-file (/small-file) | 4.81k | 14.92k | 386 MB/sec | 1.17 GB/sec | **3.10X** |
|
|
38
39
|
|
|
39
40
|
### Performance against other frameworks
|
|
40
41
|
|
|
@@ -101,7 +102,7 @@ app.listen(3000, () => {
|
|
|
101
102
|
```
|
|
102
103
|
|
|
103
104
|
- This also applies to non-SSL HTTP too. Do not create http server manually, use `app.listen()` instead.
|
|
104
|
-
-
|
|
105
|
+
- Node.JS max header size is 16384 bytes, while uWebSockets by default is 4096 bytes, so if you need longer headers set the env variable `UWS_HTTP_MAX_HEADERS_SIZE` to max byte count you need.
|
|
105
106
|
|
|
106
107
|
## Performance tips
|
|
107
108
|
|
|
@@ -327,6 +328,8 @@ Almost all middlewares that are compatible with Express are compatible with µEx
|
|
|
327
328
|
- ✅ [passport](https://www.npmjs.com/package/passport)
|
|
328
329
|
- ✅ [morgan](https://www.npmjs.com/package/morgan)
|
|
329
330
|
- ✅ [swagger-ui-express](https://www.npmjs.com/package/swagger-ui-express)
|
|
331
|
+
- ✅ [graphql-http](https://www.npmjs.com/package/graphql-http)
|
|
332
|
+
- ✅ [better-sse](https://www.npmjs.com/package/better-sse)
|
|
330
333
|
|
|
331
334
|
Middlewares and modules that are confirmed to not work:
|
|
332
335
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultimate-express",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.10",
|
|
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": {
|
|
@@ -67,6 +67,7 @@
|
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
69
69
|
"@codechecks/client": "^0.1.12",
|
|
70
|
+
"better-sse": "^0.14.1",
|
|
70
71
|
"body-parser": "^1.20.3",
|
|
71
72
|
"compression": "^1.8.0",
|
|
72
73
|
"cookie-parser": "^1.4.6",
|
|
@@ -74,6 +75,7 @@
|
|
|
74
75
|
"cors": "^2.8.5",
|
|
75
76
|
"ejs": "^3.1.10",
|
|
76
77
|
"errorhandler": "^1.5.1",
|
|
78
|
+
"eventsource": "^3.0.6",
|
|
77
79
|
"exit-hook": "^2.2.1",
|
|
78
80
|
"express": "latest-4",
|
|
79
81
|
"express-art-template": "^1.0.1",
|
|
@@ -86,6 +88,7 @@
|
|
|
86
88
|
"express-session": "^1.18.0",
|
|
87
89
|
"express-subdomain": "^1.0.6",
|
|
88
90
|
"formdata-node": "^6.0.3",
|
|
91
|
+
"graphql-http": "^1.22.4",
|
|
89
92
|
"helmet": "^8.1.0",
|
|
90
93
|
"method-override": "^3.0.0",
|
|
91
94
|
"morgan": "^1.10.0",
|
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 {
|
|
@@ -201,8 +201,18 @@ module.exports = function compileDeclarative(cb, app) {
|
|
|
201
201
|
}
|
|
202
202
|
|
|
203
203
|
// get body
|
|
204
|
+
let sendUsed = false;
|
|
204
205
|
for(let call of callExprs) {
|
|
205
206
|
if(call.obj.propertyName === 'send' || call.obj.propertyName === 'end') {
|
|
207
|
+
if(sendUsed) {
|
|
208
|
+
return false;
|
|
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
|
+
}
|
|
206
216
|
const arg = call.arguments[0];
|
|
207
217
|
if(arg) {
|
|
208
218
|
if(arg.type === 'Literal') {
|
|
@@ -212,6 +222,17 @@ module.exports = function compileDeclarative(cb, app) {
|
|
|
212
222
|
let val = arg.value;
|
|
213
223
|
if(val === null) {
|
|
214
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
|
+
}
|
|
215
236
|
}
|
|
216
237
|
body.push({type: 'text', value: val});
|
|
217
238
|
} else if(arg.type === 'TemplateLiteral') {
|
|
@@ -300,7 +321,11 @@ module.exports = function compileDeclarative(cb, app) {
|
|
|
300
321
|
return false;
|
|
301
322
|
}
|
|
302
323
|
|
|
303
|
-
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
|
+
}
|
|
304
329
|
body.push({
|
|
305
330
|
type: 'text',
|
|
306
331
|
value:
|
|
@@ -315,6 +340,7 @@ module.exports = function compileDeclarative(cb, app) {
|
|
|
315
340
|
return false;
|
|
316
341
|
}
|
|
317
342
|
}
|
|
343
|
+
sendUsed = true;
|
|
318
344
|
}
|
|
319
345
|
}
|
|
320
346
|
|
|
@@ -329,6 +355,9 @@ module.exports = function compileDeclarative(cb, app) {
|
|
|
329
355
|
if(header[0].toLowerCase() === 'content-length') {
|
|
330
356
|
return false;
|
|
331
357
|
}
|
|
358
|
+
if(header[0].toLowerCase() === 'content-type' && header[1].includes('text/') && !header[1].includes(';')) {
|
|
359
|
+
header[1] += '; charset=utf-8';
|
|
360
|
+
}
|
|
332
361
|
decRes = decRes.writeHeader(header[0], header[1]);
|
|
333
362
|
}
|
|
334
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
|
@@ -71,6 +71,7 @@ module.exports = class Response extends Writable {
|
|
|
71
71
|
#socket = null;
|
|
72
72
|
#pendingChunks = [];
|
|
73
73
|
#lastWriteChunkTime = 0;
|
|
74
|
+
#writeTimeout = null;
|
|
74
75
|
constructor(res, req, app) {
|
|
75
76
|
super();
|
|
76
77
|
this._req = req;
|
|
@@ -155,11 +156,28 @@ module.exports = class Response extends Writable {
|
|
|
155
156
|
const now = Date.now();
|
|
156
157
|
// the first chunk is sent immediately (!this.#lastWriteChunkTime)
|
|
157
158
|
// the other chunks are sent when watermark is reached (size >= HIGH_WATERMARK)
|
|
158
|
-
// or if elapsed
|
|
159
|
-
if (!this.#lastWriteChunkTime || size >= HIGH_WATERMARK || now - this.#lastWriteChunkTime >
|
|
159
|
+
// or if elapsed 50ms of last send (now - this.#lastWriteChunkTime > 50)
|
|
160
|
+
if (!this.#lastWriteChunkTime || size >= HIGH_WATERMARK || now - this.#lastWriteChunkTime > 50) {
|
|
160
161
|
this._res.write(Buffer.concat(this.#pendingChunks, size));
|
|
161
162
|
this.#pendingChunks = [];
|
|
162
163
|
this.#lastWriteChunkTime = now;
|
|
164
|
+
if(this.#writeTimeout) {
|
|
165
|
+
clearTimeout(this.#writeTimeout);
|
|
166
|
+
this.#writeTimeout = null;
|
|
167
|
+
}
|
|
168
|
+
} else if(!this.#writeTimeout) {
|
|
169
|
+
this.#writeTimeout = setTimeout(() => {
|
|
170
|
+
this.#writeTimeout = null;
|
|
171
|
+
if(!this.finished && !this.aborted) this._res.cork(() => {
|
|
172
|
+
if(this.#pendingChunks.length) {
|
|
173
|
+
const size = this.#pendingChunks.reduce((acc, chunk) => acc + chunk.byteLength, 0);
|
|
174
|
+
this._res.write(Buffer.concat(this.#pendingChunks, size));
|
|
175
|
+
this.#pendingChunks = [];
|
|
176
|
+
this.#lastWriteChunkTime = now;
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
}, 50);
|
|
180
|
+
this.#writeTimeout.unref();
|
|
163
181
|
}
|
|
164
182
|
this.writingChunk = false;
|
|
165
183
|
callback(null);
|
|
@@ -201,12 +219,13 @@ module.exports = class Response extends Writable {
|
|
|
201
219
|
this.statusText = statusMessage;
|
|
202
220
|
}
|
|
203
221
|
if(!headers) {
|
|
204
|
-
if(!statusMessage) return;
|
|
222
|
+
if(!statusMessage) return this;
|
|
205
223
|
headers = statusMessage;
|
|
206
224
|
}
|
|
207
225
|
for(let header in headers) {
|
|
208
226
|
this.set(header, headers[header]);
|
|
209
227
|
}
|
|
228
|
+
return this;
|
|
210
229
|
}
|
|
211
230
|
writeHeaders(utf8) {
|
|
212
231
|
for(const header in this.headers) {
|
|
@@ -225,9 +244,6 @@ module.exports = class Response extends Writable {
|
|
|
225
244
|
this._res.writeHeader(header, value);
|
|
226
245
|
}
|
|
227
246
|
}
|
|
228
|
-
if(!this.headers['content-type']) {
|
|
229
|
-
this._res.writeHeader('content-type', 'text/html' + (utf8 ? `; charset=utf-8` : ''));
|
|
230
|
-
}
|
|
231
247
|
this.headersSent = true;
|
|
232
248
|
}
|
|
233
249
|
_implicitHeader() {
|
|
@@ -267,6 +283,8 @@ module.exports = class Response extends Writable {
|
|
|
267
283
|
this._res.end();
|
|
268
284
|
this.finished = true;
|
|
269
285
|
if(this.socketExists) this.socket.emit('close');
|
|
286
|
+
this.emit('finish');
|
|
287
|
+
this.emit('close');
|
|
270
288
|
return;
|
|
271
289
|
}
|
|
272
290
|
}
|
|
@@ -305,6 +323,7 @@ module.exports = class Response extends Writable {
|
|
|
305
323
|
const isBuffer = Buffer.isBuffer(body);
|
|
306
324
|
if(body === null || body === undefined) {
|
|
307
325
|
body = '';
|
|
326
|
+
return this.end(body);
|
|
308
327
|
} else if(typeof body === 'object' && !isBuffer) {
|
|
309
328
|
return this.json(body);
|
|
310
329
|
} else if(typeof body === 'number') {
|
|
@@ -313,18 +332,25 @@ module.exports = class Response extends Writable {
|
|
|
313
332
|
return this.status(body).send(arguments[1]);
|
|
314
333
|
} else {
|
|
315
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
|
+
}
|
|
316
338
|
return this.sendStatus(body);
|
|
317
339
|
}
|
|
340
|
+
} else if(typeof body === 'boolean') {
|
|
341
|
+
return this.json(body);
|
|
318
342
|
} else if(!isBuffer) {
|
|
319
343
|
body = String(body);
|
|
320
344
|
}
|
|
321
|
-
if(typeof body === 'string') {
|
|
345
|
+
if(typeof body === 'string' && !isBuffer) {
|
|
322
346
|
const contentType = this.headers['content-type'];
|
|
323
347
|
if(!contentType) {
|
|
324
348
|
this.headers['content-type'] = 'text/html; charset=utf-8';
|
|
325
349
|
} else if(!contentType.includes(';')) {
|
|
326
350
|
this.headers['content-type'] += '; charset=utf-8';
|
|
327
351
|
}
|
|
352
|
+
} else {
|
|
353
|
+
this.headers['content-type'] = 'application/octet-stream';
|
|
328
354
|
}
|
|
329
355
|
return this.end(body);
|
|
330
356
|
}
|
|
@@ -593,6 +619,9 @@ module.exports = class Response extends Writable {
|
|
|
593
619
|
getHeader(field) {
|
|
594
620
|
return this.get(field);
|
|
595
621
|
}
|
|
622
|
+
getHeaders(){
|
|
623
|
+
return this.headers;
|
|
624
|
+
}
|
|
596
625
|
removeHeader(field) {
|
|
597
626
|
delete this.headers[field.toLowerCase()];
|
|
598
627
|
return this;
|
|
@@ -696,11 +725,7 @@ module.exports = class Response extends Writable {
|
|
|
696
725
|
jsonp(object) {
|
|
697
726
|
let callback = this.req.query[this.app.get('jsonp callback name')];
|
|
698
727
|
let body = stringify(object, this.app.get('json replacer'), this.app.get('json spaces'), this.app.get('json escape'));
|
|
699
|
-
|
|
700
|
-
if(!this.headers['content-type']) {
|
|
701
|
-
this.headers['content-type'] = 'application/javascript; charset=utf-8';
|
|
702
|
-
this.headers['X-Content-Type-Options'] = 'nosniff';
|
|
703
|
-
}
|
|
728
|
+
let js = false;
|
|
704
729
|
|
|
705
730
|
if(Array.isArray(callback)) {
|
|
706
731
|
callback = callback[0];
|
|
@@ -718,6 +743,13 @@ module.exports = class Response extends Writable {
|
|
|
718
743
|
.replace(/\u2029/g, '\\u2029')
|
|
719
744
|
}
|
|
720
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';
|
|
721
753
|
}
|
|
722
754
|
|
|
723
755
|
return this.send(body);
|
|
@@ -742,7 +774,25 @@ module.exports = class Response extends Writable {
|
|
|
742
774
|
this.location(url);
|
|
743
775
|
this.status(status);
|
|
744
776
|
this.headers['content-type'] = 'text/plain; charset=utf-8';
|
|
745
|
-
|
|
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
|
+
}
|
|
746
796
|
}
|
|
747
797
|
|
|
748
798
|
type(type) {
|
|
@@ -755,6 +805,11 @@ module.exports = class Response extends Writable {
|
|
|
755
805
|
contentType = this.type;
|
|
756
806
|
|
|
757
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
|
+
}
|
|
758
813
|
vary(this, field);
|
|
759
814
|
return this;
|
|
760
815
|
}
|
|
@@ -766,4 +821,4 @@ module.exports = class Response extends Writable {
|
|
|
766
821
|
get writableFinished() {
|
|
767
822
|
return this.finished;
|
|
768
823
|
}
|
|
769
|
-
}
|
|
824
|
+
}
|
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
|
}
|
|
@@ -526,6 +530,9 @@ module.exports = class Router extends EventEmitter {
|
|
|
526
530
|
req._opPath += '/';
|
|
527
531
|
}
|
|
528
532
|
const routed = await callback._routeRequest(req, res, 0);
|
|
533
|
+
if (req._error) {
|
|
534
|
+
req._errorKey = route.routeKey;
|
|
535
|
+
}
|
|
529
536
|
if(routed) return resolve(true);
|
|
530
537
|
next();
|
|
531
538
|
} else {
|
package/src/view.js
CHANGED
|
@@ -59,26 +59,25 @@ module.exports = class View {
|
|
|
59
59
|
this.path += this.ext;
|
|
60
60
|
}
|
|
61
61
|
} else {
|
|
62
|
-
this.path =
|
|
62
|
+
this.path = this.lookup(fileName);
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
lookup(name) {
|
|
67
|
-
let
|
|
67
|
+
let _path;
|
|
68
68
|
let roots = [].concat(this.root);
|
|
69
|
-
for (let i = 0; i < roots.length && !
|
|
69
|
+
for (let i = 0; i < roots.length && !_path; i++) {
|
|
70
70
|
const root = roots[i];
|
|
71
71
|
|
|
72
72
|
// resolve the path
|
|
73
73
|
const loc = path.resolve(root, name);
|
|
74
74
|
const dir = path.dirname(loc);
|
|
75
75
|
const file = path.basename(loc);
|
|
76
|
-
|
|
76
|
+
|
|
77
77
|
// resolve the file
|
|
78
|
-
|
|
78
|
+
_path = this.resolve(dir, file);
|
|
79
79
|
}
|
|
80
|
-
|
|
81
|
-
return path;
|
|
80
|
+
return _path;
|
|
82
81
|
}
|
|
83
82
|
|
|
84
83
|
// ill be real idk what exactly this does but express implements it this way
|
|
@@ -101,19 +100,19 @@ module.exports = class View {
|
|
|
101
100
|
const ext = this.ext;
|
|
102
101
|
|
|
103
102
|
// <path>.<ext>
|
|
104
|
-
let
|
|
105
|
-
let stat = tryStat(
|
|
103
|
+
let _path = path.join(dir, file);
|
|
104
|
+
let stat = tryStat(_path);
|
|
106
105
|
|
|
107
106
|
if(stat && stat.isFile()) {
|
|
108
|
-
return
|
|
107
|
+
return _path;
|
|
109
108
|
}
|
|
110
109
|
|
|
111
110
|
// <path>/index.<ext>
|
|
112
|
-
|
|
113
|
-
stat = tryStat(
|
|
111
|
+
_path = path.join(dir, path.basename(file, ext), 'index' + ext);
|
|
112
|
+
stat = tryStat(_path);
|
|
114
113
|
|
|
115
114
|
if(stat && stat.isFile()) {
|
|
116
|
-
return
|
|
115
|
+
return _path;
|
|
117
116
|
}
|
|
118
117
|
}
|
|
119
118
|
}
|