ultimate-express 1.2.15 → 1.2.16
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/EXPRESS_LICENSE +25 -25
- package/LICENSE +201 -201
- package/README.md +314 -314
- package/package.json +87 -86
- package/src/application.js +302 -302
- package/src/index.js +43 -43
- package/src/middlewares.js +316 -316
- package/src/request.js +402 -402
- package/src/response.js +762 -762
- package/src/router.js +541 -541
- package/src/utils.js +338 -338
- package/src/view.js +118 -118
- package/src/worker.js +30 -30
package/src/router.js
CHANGED
|
@@ -1,542 +1,542 @@
|
|
|
1
|
-
/*
|
|
2
|
-
Copyright 2024 dimden.dev
|
|
3
|
-
|
|
4
|
-
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
-
you may not use this file except in compliance with the License.
|
|
6
|
-
You may obtain a copy of the License at
|
|
7
|
-
|
|
8
|
-
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
-
|
|
10
|
-
Unless required by applicable law or agreed to in writing, software
|
|
11
|
-
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
-
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
-
See the License for the specific language governing permissions and
|
|
14
|
-
limitations under the License.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
const { patternToRegex, needsConversionToRegex, deprecated, findIndexStartingFrom, canBeOptimized } = require("./utils.js");
|
|
18
|
-
const Response = require("./response.js");
|
|
19
|
-
const Request = require("./request.js");
|
|
20
|
-
const { EventEmitter } = require("tseep");
|
|
21
|
-
|
|
22
|
-
let routeKey = 0;
|
|
23
|
-
|
|
24
|
-
const methods = [
|
|
25
|
-
'all',
|
|
26
|
-
'post', 'put', 'delete', 'patch', 'options', 'head', 'trace', 'connect',
|
|
27
|
-
'checkout', 'copy', 'lock', 'mkcol', 'move', 'purge', 'propfind', 'proppatch',
|
|
28
|
-
'search', 'subscribe', 'unsubscribe', 'report', 'mkactivity', 'mkcalendar',
|
|
29
|
-
'checkout', 'merge', 'm-search', 'notify', 'subscribe', 'unsubscribe', 'search'
|
|
30
|
-
];
|
|
31
|
-
const supportedUwsMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', 'HEAD', 'CONNECT', 'TRACE'];
|
|
32
|
-
|
|
33
|
-
module.exports = class Router extends EventEmitter {
|
|
34
|
-
#paramCallbacks = new Map();
|
|
35
|
-
#mountpathCache = new Map();
|
|
36
|
-
#paramFunction;
|
|
37
|
-
constructor(settings = {}) {
|
|
38
|
-
super();
|
|
39
|
-
|
|
40
|
-
this._routes = [];
|
|
41
|
-
this.errorRoute = undefined;
|
|
42
|
-
this.mountpath = '/';
|
|
43
|
-
this.settings = settings;
|
|
44
|
-
this._request = Request;
|
|
45
|
-
this._response = Response;
|
|
46
|
-
this.request = this._request.prototype;
|
|
47
|
-
this.response = this._response.prototype;
|
|
48
|
-
|
|
49
|
-
if(typeof settings.caseSensitive !== 'undefined') {
|
|
50
|
-
this.settings['case sensitive routing'] = settings.caseSensitive;
|
|
51
|
-
delete this.settings.caseSensitive;
|
|
52
|
-
}
|
|
53
|
-
if(typeof settings.strict !== 'undefined') {
|
|
54
|
-
this.settings['strict routing'] = settings.strict;
|
|
55
|
-
delete this.settings.strict;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
if(typeof this.settings['case sensitive routing'] === 'undefined') {
|
|
59
|
-
this.settings['case sensitive routing'] = true;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
for(let method of methods) {
|
|
63
|
-
this[method] = (path, ...callbacks) => {
|
|
64
|
-
this.#createRoute(method.toUpperCase(), path, this, ...callbacks);
|
|
65
|
-
};
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
get(path, ...callbacks) {
|
|
70
|
-
if(typeof path === 'string' && callbacks.length === 0) {
|
|
71
|
-
const key = path;
|
|
72
|
-
const res = this.settings[key];
|
|
73
|
-
if(typeof res === 'undefined' && this.parent) {
|
|
74
|
-
return this.parent.get(key);
|
|
75
|
-
} else {
|
|
76
|
-
return res;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
return this.#createRoute('GET', path, this, ...callbacks);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
del(path, ...callbacks) {
|
|
83
|
-
deprecated('app.del', 'app.delete');
|
|
84
|
-
return this.#createRoute('DELETE', path, this, ...callbacks);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
getFullMountpath(req) {
|
|
88
|
-
let fullStack = req._stack.join("");
|
|
89
|
-
let fullMountpath = this.#mountpathCache.get(fullStack);
|
|
90
|
-
if(!fullMountpath) {
|
|
91
|
-
fullMountpath = patternToRegex(fullStack, true);
|
|
92
|
-
this.#mountpathCache.set(fullStack, fullMountpath);
|
|
93
|
-
}
|
|
94
|
-
return fullMountpath;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
#pathMatches(route, req) {
|
|
98
|
-
let path = req._opPath;
|
|
99
|
-
let pattern = route.pattern;
|
|
100
|
-
|
|
101
|
-
if (typeof pattern === 'string') {
|
|
102
|
-
if(pattern === '/*') {
|
|
103
|
-
return true;
|
|
104
|
-
}
|
|
105
|
-
if(path === '') {
|
|
106
|
-
path = '/';
|
|
107
|
-
}
|
|
108
|
-
if(!this.get('case sensitive routing')) {
|
|
109
|
-
path = path.toLowerCase();
|
|
110
|
-
pattern = pattern.toLowerCase();
|
|
111
|
-
}
|
|
112
|
-
return pattern === path;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
return pattern.test(path);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
#createRoute(method, path, parent = this, ...callbacks) {
|
|
119
|
-
callbacks = callbacks.flat();
|
|
120
|
-
const paths = Array.isArray(path) ? path : [path];
|
|
121
|
-
const routes = [];
|
|
122
|
-
for(let path of paths) {
|
|
123
|
-
if(!this.get('strict routing') && typeof path === 'string' && path.endsWith('/') && path !== '/') {
|
|
124
|
-
path = path.slice(0, -1);
|
|
125
|
-
}
|
|
126
|
-
if(path === '*') {
|
|
127
|
-
path = '/*';
|
|
128
|
-
}
|
|
129
|
-
const route = {
|
|
130
|
-
method: method === 'USE' ? 'ALL' : method.toUpperCase(),
|
|
131
|
-
path,
|
|
132
|
-
pattern: method === 'USE' || needsConversionToRegex(path) ? patternToRegex(path, method === 'USE') : path,
|
|
133
|
-
callbacks,
|
|
134
|
-
routeKey: routeKey++,
|
|
135
|
-
use: method === 'USE',
|
|
136
|
-
all: method === 'ALL' || method === 'USE',
|
|
137
|
-
gettable: method === 'GET' || method === 'HEAD',
|
|
138
|
-
};
|
|
139
|
-
routes.push(route);
|
|
140
|
-
// normal routes optimization
|
|
141
|
-
if(canBeOptimized(route.path) && route.pattern !== '/*' && !this.parent && this.get('case sensitive routing') && this.uwsApp) {
|
|
142
|
-
if(supportedUwsMethods.includes(method)) {
|
|
143
|
-
const optimizedPath = this.#optimizeRoute(route, this._routes);
|
|
144
|
-
if(optimizedPath) {
|
|
145
|
-
this.#registerUwsRoute(route, optimizedPath);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
// router optimization
|
|
150
|
-
if(
|
|
151
|
-
route.use && !needsConversionToRegex(path) && path !== '/*' && // must be predictable path
|
|
152
|
-
this.get('case sensitive routing') && // uWS only supports case sensitive routing
|
|
153
|
-
callbacks.filter(c => c instanceof Router).length === 1 && // only 1 router can be optimized per route
|
|
154
|
-
callbacks[callbacks.length - 1] instanceof Router // the router must be the last callback
|
|
155
|
-
) {
|
|
156
|
-
let callbacksBeforeRouter = [];
|
|
157
|
-
for(let callback of callbacks) {
|
|
158
|
-
if(callback instanceof Router) {
|
|
159
|
-
// get optimized path to router
|
|
160
|
-
let optimizedPathToRouter = this.#optimizeRoute(route, this._routes);
|
|
161
|
-
if(!optimizedPathToRouter) {
|
|
162
|
-
break;
|
|
163
|
-
}
|
|
164
|
-
optimizedPathToRouter = optimizedPathToRouter.slice(0, -1); // remove last element, which is the router itself
|
|
165
|
-
if(optimizedPathToRouter) {
|
|
166
|
-
// wait for routes in router to be registered
|
|
167
|
-
setTimeout(() => {
|
|
168
|
-
if(!this.listenCalled) {
|
|
169
|
-
return; // can only optimize router whos parent is listening
|
|
170
|
-
}
|
|
171
|
-
for(let cbroute of callback._routes) {
|
|
172
|
-
if(!needsConversionToRegex(cbroute.path) && cbroute.path !== '/*' && supportedUwsMethods.includes(cbroute.method)) {
|
|
173
|
-
let optimizedRouterPath = this.#optimizeRoute(cbroute, callback._routes);
|
|
174
|
-
if(optimizedRouterPath) {
|
|
175
|
-
optimizedRouterPath = optimizedRouterPath.slice(0, -1);
|
|
176
|
-
const optimizedPath = [...optimizedPathToRouter, {
|
|
177
|
-
// fake route to update req._opPath and req.url
|
|
178
|
-
...route,
|
|
179
|
-
callbacks: [
|
|
180
|
-
(req, res, next) => {
|
|
181
|
-
next('skipPop');
|
|
182
|
-
}
|
|
183
|
-
]
|
|
184
|
-
}, ...optimizedRouterPath];
|
|
185
|
-
this.#registerUwsRoute({
|
|
186
|
-
...cbroute,
|
|
187
|
-
path: route.path + cbroute.path,
|
|
188
|
-
pattern: route.path + cbroute.path,
|
|
189
|
-
optimizedRouter: true
|
|
190
|
-
}, optimizedPath);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
}, 100);
|
|
195
|
-
}
|
|
196
|
-
// only 1 router can be optimized per route
|
|
197
|
-
break;
|
|
198
|
-
} else {
|
|
199
|
-
callbacksBeforeRouter.push(route);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
this._routes.push(...routes);
|
|
205
|
-
|
|
206
|
-
return parent;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// if route is a simple string, its possible to pre-calculate its path
|
|
210
|
-
// and then create a native uWS route for it, which is much faster
|
|
211
|
-
#optimizeRoute(route, routes) {
|
|
212
|
-
const optimizedPath = [];
|
|
213
|
-
|
|
214
|
-
for(let i = 0; i < routes.length; i++) {
|
|
215
|
-
const r = routes[i];
|
|
216
|
-
if(r.routeKey > route.routeKey) {
|
|
217
|
-
break;
|
|
218
|
-
}
|
|
219
|
-
// if the methods are not the same, and its not an all method, skip it
|
|
220
|
-
if(!r.all && r.method !== route.method) {
|
|
221
|
-
// check if the methods are compatible (GET and HEAD)
|
|
222
|
-
if(!(r.method === 'HEAD' && route.method === 'GET')) {
|
|
223
|
-
continue;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// check if the paths match
|
|
228
|
-
if(
|
|
229
|
-
(r.pattern instanceof RegExp && r.pattern.test(route.path)) ||
|
|
230
|
-
(typeof r.pattern === 'string' && (r.pattern === route.path || r.pattern === '/*'))
|
|
231
|
-
) {
|
|
232
|
-
if(r.callbacks.some(c => c instanceof Router)) {
|
|
233
|
-
return false; // cant optimize nested routers with matches
|
|
234
|
-
}
|
|
235
|
-
optimizedPath.push(r);
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
optimizedPath.push(route);
|
|
239
|
-
|
|
240
|
-
return optimizedPath;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
handleRequest(res, req) {
|
|
244
|
-
const request = new this._request(req, res, this);
|
|
245
|
-
const response = new this._response(res, request, this);
|
|
246
|
-
request.res = response;
|
|
247
|
-
response.req = request;
|
|
248
|
-
res.onAborted(() => {
|
|
249
|
-
const err = new Error('Connection closed');
|
|
250
|
-
err.code = 'ECONNRESET';
|
|
251
|
-
response.aborted = true;
|
|
252
|
-
response.socket.emit('error', err);
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
return { request, response };
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
#registerUwsRoute(route, optimizedPath) {
|
|
259
|
-
let method = route.method.toLowerCase();
|
|
260
|
-
if(method === 'all') {
|
|
261
|
-
method = 'any';
|
|
262
|
-
} else if(method === 'delete') {
|
|
263
|
-
method = 'del';
|
|
264
|
-
}
|
|
265
|
-
if(!route.optimizedRouter && route.path.includes(":")) {
|
|
266
|
-
route.optimizedParams = route.path.match(/:(\w+)/g).map(p => p.slice(1));
|
|
267
|
-
}
|
|
268
|
-
const fn = async (res, req) => {
|
|
269
|
-
const { request, response } = this.handleRequest(res, req);
|
|
270
|
-
if(route.optimizedParams) {
|
|
271
|
-
request.optimizedParams = {};
|
|
272
|
-
for(let i = 0; i < route.optimizedParams.length; i++) {
|
|
273
|
-
request.optimizedParams[route.optimizedParams[i]] = req.getParameter(i);
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
const matchedRoute = await this._routeRequest(request, response, 0, optimizedPath, true, route);
|
|
277
|
-
if(!matchedRoute && !response.headersSent && !response.aborted) {
|
|
278
|
-
response.status(404);
|
|
279
|
-
response.send(this._generateErrorPage(`Cannot ${request.method} ${request.path}`, false));
|
|
280
|
-
}
|
|
281
|
-
};
|
|
282
|
-
route.optimizedPath = optimizedPath;
|
|
283
|
-
let replacedPath = route.path.replace(/:(\w+)/g, ':x');
|
|
284
|
-
this.uwsApp[method](replacedPath, fn);
|
|
285
|
-
if(!this.get('strict routing') && route.path[route.path.length - 1] !== '/') {
|
|
286
|
-
this.uwsApp[method](replacedPath + '/', fn);
|
|
287
|
-
if(method === 'get') {
|
|
288
|
-
this.uwsApp.head(replacedPath + '/', fn);
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
if(method === 'get') {
|
|
292
|
-
this.uwsApp.head(replacedPath, fn);
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
#handleError(err, request, response) {
|
|
297
|
-
let errorRoute = this.errorRoute, parent = this.parent;
|
|
298
|
-
while(!errorRoute && parent) {
|
|
299
|
-
errorRoute = parent.errorRoute;
|
|
300
|
-
parent = parent.parent;
|
|
301
|
-
}
|
|
302
|
-
if(errorRoute) {
|
|
303
|
-
return errorRoute(err, request, response, () => {
|
|
304
|
-
if(!response.headersSent) {
|
|
305
|
-
if(response.statusCode === 200) {
|
|
306
|
-
response.statusCode = 500;
|
|
307
|
-
}
|
|
308
|
-
response.send(this._generateErrorPage(err, true));
|
|
309
|
-
}
|
|
310
|
-
});
|
|
311
|
-
}
|
|
312
|
-
console.error(err);
|
|
313
|
-
if(response.statusCode === 200) {
|
|
314
|
-
response.statusCode = 500;
|
|
315
|
-
}
|
|
316
|
-
response.send(this._generateErrorPage(err, true));
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
#extractParams(pattern, path) {
|
|
320
|
-
let match = pattern.exec(path);
|
|
321
|
-
const obj = match?.groups ?? {};
|
|
322
|
-
for(let i = 1; i < match.length; i++) {
|
|
323
|
-
obj[i - 1] = match[i];
|
|
324
|
-
}
|
|
325
|
-
return obj;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
#preprocessRequest(req, res, route) {
|
|
329
|
-
req.route = route;
|
|
330
|
-
if(route.optimizedParams) {
|
|
331
|
-
req.params = req.optimizedParams;
|
|
332
|
-
} else if(typeof route.path === 'string' && (route.path.includes(':') || route.path.includes('*')) && route.pattern instanceof RegExp) {
|
|
333
|
-
let path = req.path;
|
|
334
|
-
if(req._stack.length > 0) {
|
|
335
|
-
path = path.replace(this.getFullMountpath(req), '');
|
|
336
|
-
}
|
|
337
|
-
req.params = this.#extractParams(route.pattern, path);
|
|
338
|
-
if(req._paramStack.length > 0) {
|
|
339
|
-
for(let params of req._paramStack) {
|
|
340
|
-
req.params = {...params, ...req.params};
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
} else {
|
|
344
|
-
req.params = {};
|
|
345
|
-
if(req._paramStack.length > 0) {
|
|
346
|
-
for(let params of req._paramStack) {
|
|
347
|
-
req.params = {...params, ...req.params};
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
if(this.#paramCallbacks.size > 0) {
|
|
353
|
-
return new Promise(async resolve => {
|
|
354
|
-
for(let param in req.params) {
|
|
355
|
-
if(this.#paramCallbacks.has(param) && !req._gotParams.has(param)) {
|
|
356
|
-
req._gotParams.add(param);
|
|
357
|
-
const pcs = this.#paramCallbacks.get(param);
|
|
358
|
-
for(let i = 0; i < pcs.length; i++) {
|
|
359
|
-
const fn = pcs[i];
|
|
360
|
-
await new Promise(resolveRoute => {
|
|
361
|
-
const next = (thingamabob) => {
|
|
362
|
-
if(thingamabob) {
|
|
363
|
-
if(thingamabob === 'route') {
|
|
364
|
-
return resolve('route');
|
|
365
|
-
} else {
|
|
366
|
-
this.#handleError(thingamabob, req, res);
|
|
367
|
-
return resolve(false);
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
return resolveRoute();
|
|
371
|
-
};
|
|
372
|
-
req.next = next;
|
|
373
|
-
fn(req, res, next, req.params[param], param);
|
|
374
|
-
});
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
resolve(true)
|
|
380
|
-
});
|
|
381
|
-
}
|
|
382
|
-
return true;
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
param(name, fn) {
|
|
386
|
-
if(typeof name === 'function') {
|
|
387
|
-
deprecated('app.param(callback)', 'app.param(name, callback)', true);
|
|
388
|
-
this.#paramFunction = name;
|
|
389
|
-
} else {
|
|
390
|
-
if(this.#paramFunction) {
|
|
391
|
-
if(!this.#paramCallbacks.has(name)) {
|
|
392
|
-
this.#paramCallbacks.set(name, []);
|
|
393
|
-
}
|
|
394
|
-
this.#paramCallbacks.get(name).push(this.#paramFunction(name, fn));
|
|
395
|
-
} else {
|
|
396
|
-
let names = Array.isArray(name) ? name : [name];
|
|
397
|
-
for(let name of names) {
|
|
398
|
-
if(!this.#paramCallbacks.has(name)) {
|
|
399
|
-
this.#paramCallbacks.set(name, []);
|
|
400
|
-
}
|
|
401
|
-
this.#paramCallbacks.get(name).push(fn);
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
async _routeRequest(req, res, startIndex = 0, routes = this._routes, skipCheck = false, skipUntil) {
|
|
408
|
-
return new Promise(async (resolve) => {
|
|
409
|
-
let routeIndex = skipCheck ? startIndex : findIndexStartingFrom(routes, r => (r.all || r.method === req.method || (r.gettable && req.method === 'HEAD')) && this.#pathMatches(r, req), startIndex);
|
|
410
|
-
const route = routes[routeIndex];
|
|
411
|
-
if(!route) {
|
|
412
|
-
if(!skipCheck) {
|
|
413
|
-
// on normal unoptimized routes, if theres no match then there is no route
|
|
414
|
-
return resolve(false);
|
|
415
|
-
}
|
|
416
|
-
// on optimized routes, there can be more routes, so we have to use unoptimized routing and skip until we find route we stopped at
|
|
417
|
-
return resolve(this._routeRequest(req, res, 0, this._routes, false, skipUntil));
|
|
418
|
-
}
|
|
419
|
-
let callbackindex = 0;
|
|
420
|
-
let continueRoute = await this.#preprocessRequest(req, res, route);
|
|
421
|
-
if(route.use) {
|
|
422
|
-
req._stack.push(route.path);
|
|
423
|
-
req._opPath =
|
|
424
|
-
req.path.replace(this.getFullMountpath(req), '') +
|
|
425
|
-
(req.endsWithSlash && req.path !== '/' && this.get('strict routing') ? '/' : '');
|
|
426
|
-
req.url = req._opPath + req.urlQuery;
|
|
427
|
-
if(req.url === '') req.url = '/';
|
|
428
|
-
}
|
|
429
|
-
const next = async (thingamabob) => {
|
|
430
|
-
if(thingamabob) {
|
|
431
|
-
if(thingamabob === 'route' || thingamabob === 'skipPop') {
|
|
432
|
-
if(route.use && thingamabob !== 'skipPop') {
|
|
433
|
-
req._stack.pop();
|
|
434
|
-
req._opPath =
|
|
435
|
-
(req._stack.length > 0 ? req.path.replace(this.getFullMountpath(req), '') : req.path) +
|
|
436
|
-
(req.endsWithSlash && req.path !== '/' && this.get('strict routing') ? '/' : '');
|
|
437
|
-
req.url = req._opPath + req.urlQuery;
|
|
438
|
-
if(req.url === '') req.url = '/';
|
|
439
|
-
if(req.app.parent && route.callback.constructor.name === 'Application') {
|
|
440
|
-
req.app = req.app.parent;
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
return resolve(this._routeRequest(req, res, routeIndex + 1, routes, skipCheck, skipUntil));
|
|
444
|
-
} else {
|
|
445
|
-
this.#handleError(thingamabob, req, res);
|
|
446
|
-
return resolve(true);
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
const callback = route.callbacks[callbackindex++];
|
|
450
|
-
if(!callback) {
|
|
451
|
-
return next('route');
|
|
452
|
-
}
|
|
453
|
-
if(callback instanceof Router) {
|
|
454
|
-
if(callback.constructor.name === 'Application') {
|
|
455
|
-
req.app = callback;
|
|
456
|
-
}
|
|
457
|
-
if(callback.settings.mergeParams) {
|
|
458
|
-
req._paramStack.push(req.params);
|
|
459
|
-
}
|
|
460
|
-
const routed = await callback._routeRequest(req, res, 0);
|
|
461
|
-
if(routed) return resolve(true);
|
|
462
|
-
next();
|
|
463
|
-
} else {
|
|
464
|
-
try {
|
|
465
|
-
// skipping routes we already went through via optimized path
|
|
466
|
-
if(!skipCheck && skipUntil && skipUntil.routeKey >= route.routeKey) {
|
|
467
|
-
return next();
|
|
468
|
-
}
|
|
469
|
-
const out = callback(req, res, next);
|
|
470
|
-
if(out instanceof Promise) {
|
|
471
|
-
out.catch(err => {
|
|
472
|
-
throw err;
|
|
473
|
-
});
|
|
474
|
-
}
|
|
475
|
-
} catch(err) {
|
|
476
|
-
this.#handleError(err, req, res);
|
|
477
|
-
return resolve(true);
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
req.next = next;
|
|
482
|
-
if(continueRoute === 'route') {
|
|
483
|
-
next('route');
|
|
484
|
-
} else if(continueRoute) {
|
|
485
|
-
next();
|
|
486
|
-
} else {
|
|
487
|
-
resolve(true);
|
|
488
|
-
}
|
|
489
|
-
});
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
use(path, ...callbacks) {
|
|
493
|
-
if(typeof path === 'function' || path instanceof Router || (Array.isArray(path) && path.every(p => typeof p === 'function' || p instanceof Router))) {
|
|
494
|
-
if(callbacks.length === 0 && typeof path === 'function' && path.length === 4) {
|
|
495
|
-
this.errorRoute = path;
|
|
496
|
-
return;
|
|
497
|
-
}
|
|
498
|
-
callbacks.unshift(path);
|
|
499
|
-
path = '';
|
|
500
|
-
}
|
|
501
|
-
if(path === '/') {
|
|
502
|
-
path = '';
|
|
503
|
-
}
|
|
504
|
-
for(let callback of callbacks) {
|
|
505
|
-
if(callback instanceof Router) {
|
|
506
|
-
callback.mountpath = path;
|
|
507
|
-
callback.parent = this;
|
|
508
|
-
callback.emit('mount', this);
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
this.#createRoute('USE', path, this, ...callbacks);
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
route(path) {
|
|
515
|
-
let fns = {};
|
|
516
|
-
for(let method of methods) {
|
|
517
|
-
fns[method] = (...callbacks) => {
|
|
518
|
-
return this.#createRoute(method.toUpperCase(), path, fns, ...callbacks);
|
|
519
|
-
};
|
|
520
|
-
}
|
|
521
|
-
fns.get = (...callbacks) => {
|
|
522
|
-
return this.#createRoute('GET', path, fns, ...callbacks);
|
|
523
|
-
};
|
|
524
|
-
return fns;
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
_generateErrorPage(err, checkEnv = false) {
|
|
528
|
-
if(checkEnv && this.get('env') === 'production') {
|
|
529
|
-
err = 'Internal Server Error';
|
|
530
|
-
}
|
|
531
|
-
return `<!DOCTYPE html>\n` +
|
|
532
|
-
`<html lang="en">\n` +
|
|
533
|
-
`<head>\n` +
|
|
534
|
-
`<meta charset="utf-8">\n` +
|
|
535
|
-
`<title>Error</title>\n` +
|
|
536
|
-
`</head>\n` +
|
|
537
|
-
`<body>\n` +
|
|
538
|
-
`<pre>${err?.stack ?? err}</pre>\n` +
|
|
539
|
-
`</body>\n` +
|
|
540
|
-
`</html>\n`;
|
|
541
|
-
}
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2024 dimden.dev
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const { patternToRegex, needsConversionToRegex, deprecated, findIndexStartingFrom, canBeOptimized } = require("./utils.js");
|
|
18
|
+
const Response = require("./response.js");
|
|
19
|
+
const Request = require("./request.js");
|
|
20
|
+
const { EventEmitter } = require("tseep");
|
|
21
|
+
|
|
22
|
+
let routeKey = 0;
|
|
23
|
+
|
|
24
|
+
const methods = [
|
|
25
|
+
'all',
|
|
26
|
+
'post', 'put', 'delete', 'patch', 'options', 'head', 'trace', 'connect',
|
|
27
|
+
'checkout', 'copy', 'lock', 'mkcol', 'move', 'purge', 'propfind', 'proppatch',
|
|
28
|
+
'search', 'subscribe', 'unsubscribe', 'report', 'mkactivity', 'mkcalendar',
|
|
29
|
+
'checkout', 'merge', 'm-search', 'notify', 'subscribe', 'unsubscribe', 'search'
|
|
30
|
+
];
|
|
31
|
+
const supportedUwsMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', 'HEAD', 'CONNECT', 'TRACE'];
|
|
32
|
+
|
|
33
|
+
module.exports = class Router extends EventEmitter {
|
|
34
|
+
#paramCallbacks = new Map();
|
|
35
|
+
#mountpathCache = new Map();
|
|
36
|
+
#paramFunction;
|
|
37
|
+
constructor(settings = {}) {
|
|
38
|
+
super();
|
|
39
|
+
|
|
40
|
+
this._routes = [];
|
|
41
|
+
this.errorRoute = undefined;
|
|
42
|
+
this.mountpath = '/';
|
|
43
|
+
this.settings = settings;
|
|
44
|
+
this._request = Request;
|
|
45
|
+
this._response = Response;
|
|
46
|
+
this.request = this._request.prototype;
|
|
47
|
+
this.response = this._response.prototype;
|
|
48
|
+
|
|
49
|
+
if(typeof settings.caseSensitive !== 'undefined') {
|
|
50
|
+
this.settings['case sensitive routing'] = settings.caseSensitive;
|
|
51
|
+
delete this.settings.caseSensitive;
|
|
52
|
+
}
|
|
53
|
+
if(typeof settings.strict !== 'undefined') {
|
|
54
|
+
this.settings['strict routing'] = settings.strict;
|
|
55
|
+
delete this.settings.strict;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if(typeof this.settings['case sensitive routing'] === 'undefined') {
|
|
59
|
+
this.settings['case sensitive routing'] = true;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
for(let method of methods) {
|
|
63
|
+
this[method] = (path, ...callbacks) => {
|
|
64
|
+
this.#createRoute(method.toUpperCase(), path, this, ...callbacks);
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
get(path, ...callbacks) {
|
|
70
|
+
if(typeof path === 'string' && callbacks.length === 0) {
|
|
71
|
+
const key = path;
|
|
72
|
+
const res = this.settings[key];
|
|
73
|
+
if(typeof res === 'undefined' && this.parent) {
|
|
74
|
+
return this.parent.get(key);
|
|
75
|
+
} else {
|
|
76
|
+
return res;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return this.#createRoute('GET', path, this, ...callbacks);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
del(path, ...callbacks) {
|
|
83
|
+
deprecated('app.del', 'app.delete');
|
|
84
|
+
return this.#createRoute('DELETE', path, this, ...callbacks);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
getFullMountpath(req) {
|
|
88
|
+
let fullStack = req._stack.join("");
|
|
89
|
+
let fullMountpath = this.#mountpathCache.get(fullStack);
|
|
90
|
+
if(!fullMountpath) {
|
|
91
|
+
fullMountpath = patternToRegex(fullStack, true);
|
|
92
|
+
this.#mountpathCache.set(fullStack, fullMountpath);
|
|
93
|
+
}
|
|
94
|
+
return fullMountpath;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
#pathMatches(route, req) {
|
|
98
|
+
let path = req._opPath;
|
|
99
|
+
let pattern = route.pattern;
|
|
100
|
+
|
|
101
|
+
if (typeof pattern === 'string') {
|
|
102
|
+
if(pattern === '/*') {
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
if(path === '') {
|
|
106
|
+
path = '/';
|
|
107
|
+
}
|
|
108
|
+
if(!this.get('case sensitive routing')) {
|
|
109
|
+
path = path.toLowerCase();
|
|
110
|
+
pattern = pattern.toLowerCase();
|
|
111
|
+
}
|
|
112
|
+
return pattern === path;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return pattern.test(path);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
#createRoute(method, path, parent = this, ...callbacks) {
|
|
119
|
+
callbacks = callbacks.flat();
|
|
120
|
+
const paths = Array.isArray(path) ? path : [path];
|
|
121
|
+
const routes = [];
|
|
122
|
+
for(let path of paths) {
|
|
123
|
+
if(!this.get('strict routing') && typeof path === 'string' && path.endsWith('/') && path !== '/') {
|
|
124
|
+
path = path.slice(0, -1);
|
|
125
|
+
}
|
|
126
|
+
if(path === '*') {
|
|
127
|
+
path = '/*';
|
|
128
|
+
}
|
|
129
|
+
const route = {
|
|
130
|
+
method: method === 'USE' ? 'ALL' : method.toUpperCase(),
|
|
131
|
+
path,
|
|
132
|
+
pattern: method === 'USE' || needsConversionToRegex(path) ? patternToRegex(path, method === 'USE') : path,
|
|
133
|
+
callbacks,
|
|
134
|
+
routeKey: routeKey++,
|
|
135
|
+
use: method === 'USE',
|
|
136
|
+
all: method === 'ALL' || method === 'USE',
|
|
137
|
+
gettable: method === 'GET' || method === 'HEAD',
|
|
138
|
+
};
|
|
139
|
+
routes.push(route);
|
|
140
|
+
// normal routes optimization
|
|
141
|
+
if(canBeOptimized(route.path) && route.pattern !== '/*' && !this.parent && this.get('case sensitive routing') && this.uwsApp) {
|
|
142
|
+
if(supportedUwsMethods.includes(method)) {
|
|
143
|
+
const optimizedPath = this.#optimizeRoute(route, this._routes);
|
|
144
|
+
if(optimizedPath) {
|
|
145
|
+
this.#registerUwsRoute(route, optimizedPath);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// router optimization
|
|
150
|
+
if(
|
|
151
|
+
route.use && !needsConversionToRegex(path) && path !== '/*' && // must be predictable path
|
|
152
|
+
this.get('case sensitive routing') && // uWS only supports case sensitive routing
|
|
153
|
+
callbacks.filter(c => c instanceof Router).length === 1 && // only 1 router can be optimized per route
|
|
154
|
+
callbacks[callbacks.length - 1] instanceof Router // the router must be the last callback
|
|
155
|
+
) {
|
|
156
|
+
let callbacksBeforeRouter = [];
|
|
157
|
+
for(let callback of callbacks) {
|
|
158
|
+
if(callback instanceof Router) {
|
|
159
|
+
// get optimized path to router
|
|
160
|
+
let optimizedPathToRouter = this.#optimizeRoute(route, this._routes);
|
|
161
|
+
if(!optimizedPathToRouter) {
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
optimizedPathToRouter = optimizedPathToRouter.slice(0, -1); // remove last element, which is the router itself
|
|
165
|
+
if(optimizedPathToRouter) {
|
|
166
|
+
// wait for routes in router to be registered
|
|
167
|
+
setTimeout(() => {
|
|
168
|
+
if(!this.listenCalled) {
|
|
169
|
+
return; // can only optimize router whos parent is listening
|
|
170
|
+
}
|
|
171
|
+
for(let cbroute of callback._routes) {
|
|
172
|
+
if(!needsConversionToRegex(cbroute.path) && cbroute.path !== '/*' && supportedUwsMethods.includes(cbroute.method)) {
|
|
173
|
+
let optimizedRouterPath = this.#optimizeRoute(cbroute, callback._routes);
|
|
174
|
+
if(optimizedRouterPath) {
|
|
175
|
+
optimizedRouterPath = optimizedRouterPath.slice(0, -1);
|
|
176
|
+
const optimizedPath = [...optimizedPathToRouter, {
|
|
177
|
+
// fake route to update req._opPath and req.url
|
|
178
|
+
...route,
|
|
179
|
+
callbacks: [
|
|
180
|
+
(req, res, next) => {
|
|
181
|
+
next('skipPop');
|
|
182
|
+
}
|
|
183
|
+
]
|
|
184
|
+
}, ...optimizedRouterPath];
|
|
185
|
+
this.#registerUwsRoute({
|
|
186
|
+
...cbroute,
|
|
187
|
+
path: route.path + cbroute.path,
|
|
188
|
+
pattern: route.path + cbroute.path,
|
|
189
|
+
optimizedRouter: true
|
|
190
|
+
}, optimizedPath);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}, 100);
|
|
195
|
+
}
|
|
196
|
+
// only 1 router can be optimized per route
|
|
197
|
+
break;
|
|
198
|
+
} else {
|
|
199
|
+
callbacksBeforeRouter.push(route);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
this._routes.push(...routes);
|
|
205
|
+
|
|
206
|
+
return parent;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// if route is a simple string, its possible to pre-calculate its path
|
|
210
|
+
// and then create a native uWS route for it, which is much faster
|
|
211
|
+
#optimizeRoute(route, routes) {
|
|
212
|
+
const optimizedPath = [];
|
|
213
|
+
|
|
214
|
+
for(let i = 0; i < routes.length; i++) {
|
|
215
|
+
const r = routes[i];
|
|
216
|
+
if(r.routeKey > route.routeKey) {
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
// if the methods are not the same, and its not an all method, skip it
|
|
220
|
+
if(!r.all && r.method !== route.method) {
|
|
221
|
+
// check if the methods are compatible (GET and HEAD)
|
|
222
|
+
if(!(r.method === 'HEAD' && route.method === 'GET')) {
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// check if the paths match
|
|
228
|
+
if(
|
|
229
|
+
(r.pattern instanceof RegExp && r.pattern.test(route.path)) ||
|
|
230
|
+
(typeof r.pattern === 'string' && (r.pattern === route.path || r.pattern === '/*'))
|
|
231
|
+
) {
|
|
232
|
+
if(r.callbacks.some(c => c instanceof Router)) {
|
|
233
|
+
return false; // cant optimize nested routers with matches
|
|
234
|
+
}
|
|
235
|
+
optimizedPath.push(r);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
optimizedPath.push(route);
|
|
239
|
+
|
|
240
|
+
return optimizedPath;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
handleRequest(res, req) {
|
|
244
|
+
const request = new this._request(req, res, this);
|
|
245
|
+
const response = new this._response(res, request, this);
|
|
246
|
+
request.res = response;
|
|
247
|
+
response.req = request;
|
|
248
|
+
res.onAborted(() => {
|
|
249
|
+
const err = new Error('Connection closed');
|
|
250
|
+
err.code = 'ECONNRESET';
|
|
251
|
+
response.aborted = true;
|
|
252
|
+
response.socket.emit('error', err);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
return { request, response };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
#registerUwsRoute(route, optimizedPath) {
|
|
259
|
+
let method = route.method.toLowerCase();
|
|
260
|
+
if(method === 'all') {
|
|
261
|
+
method = 'any';
|
|
262
|
+
} else if(method === 'delete') {
|
|
263
|
+
method = 'del';
|
|
264
|
+
}
|
|
265
|
+
if(!route.optimizedRouter && route.path.includes(":")) {
|
|
266
|
+
route.optimizedParams = route.path.match(/:(\w+)/g).map(p => p.slice(1));
|
|
267
|
+
}
|
|
268
|
+
const fn = async (res, req) => {
|
|
269
|
+
const { request, response } = this.handleRequest(res, req);
|
|
270
|
+
if(route.optimizedParams) {
|
|
271
|
+
request.optimizedParams = {};
|
|
272
|
+
for(let i = 0; i < route.optimizedParams.length; i++) {
|
|
273
|
+
request.optimizedParams[route.optimizedParams[i]] = req.getParameter(i);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
const matchedRoute = await this._routeRequest(request, response, 0, optimizedPath, true, route);
|
|
277
|
+
if(!matchedRoute && !response.headersSent && !response.aborted) {
|
|
278
|
+
response.status(404);
|
|
279
|
+
response.send(this._generateErrorPage(`Cannot ${request.method} ${request.path}`, false));
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
route.optimizedPath = optimizedPath;
|
|
283
|
+
let replacedPath = route.path.replace(/:(\w+)/g, ':x');
|
|
284
|
+
this.uwsApp[method](replacedPath, fn);
|
|
285
|
+
if(!this.get('strict routing') && route.path[route.path.length - 1] !== '/') {
|
|
286
|
+
this.uwsApp[method](replacedPath + '/', fn);
|
|
287
|
+
if(method === 'get') {
|
|
288
|
+
this.uwsApp.head(replacedPath + '/', fn);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if(method === 'get') {
|
|
292
|
+
this.uwsApp.head(replacedPath, fn);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
#handleError(err, request, response) {
|
|
297
|
+
let errorRoute = this.errorRoute, parent = this.parent;
|
|
298
|
+
while(!errorRoute && parent) {
|
|
299
|
+
errorRoute = parent.errorRoute;
|
|
300
|
+
parent = parent.parent;
|
|
301
|
+
}
|
|
302
|
+
if(errorRoute) {
|
|
303
|
+
return errorRoute(err, request, response, () => {
|
|
304
|
+
if(!response.headersSent) {
|
|
305
|
+
if(response.statusCode === 200) {
|
|
306
|
+
response.statusCode = 500;
|
|
307
|
+
}
|
|
308
|
+
response.send(this._generateErrorPage(err, true));
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
console.error(err);
|
|
313
|
+
if(response.statusCode === 200) {
|
|
314
|
+
response.statusCode = 500;
|
|
315
|
+
}
|
|
316
|
+
response.send(this._generateErrorPage(err, true));
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
#extractParams(pattern, path) {
|
|
320
|
+
let match = pattern.exec(path);
|
|
321
|
+
const obj = match?.groups ?? {};
|
|
322
|
+
for(let i = 1; i < match.length; i++) {
|
|
323
|
+
obj[i - 1] = match[i];
|
|
324
|
+
}
|
|
325
|
+
return obj;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
#preprocessRequest(req, res, route) {
|
|
329
|
+
req.route = route;
|
|
330
|
+
if(route.optimizedParams) {
|
|
331
|
+
req.params = req.optimizedParams;
|
|
332
|
+
} else if(typeof route.path === 'string' && (route.path.includes(':') || route.path.includes('*')) && route.pattern instanceof RegExp) {
|
|
333
|
+
let path = req.path;
|
|
334
|
+
if(req._stack.length > 0) {
|
|
335
|
+
path = path.replace(this.getFullMountpath(req), '');
|
|
336
|
+
}
|
|
337
|
+
req.params = this.#extractParams(route.pattern, path);
|
|
338
|
+
if(req._paramStack.length > 0) {
|
|
339
|
+
for(let params of req._paramStack) {
|
|
340
|
+
req.params = {...params, ...req.params};
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
} else {
|
|
344
|
+
req.params = {};
|
|
345
|
+
if(req._paramStack.length > 0) {
|
|
346
|
+
for(let params of req._paramStack) {
|
|
347
|
+
req.params = {...params, ...req.params};
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if(this.#paramCallbacks.size > 0) {
|
|
353
|
+
return new Promise(async resolve => {
|
|
354
|
+
for(let param in req.params) {
|
|
355
|
+
if(this.#paramCallbacks.has(param) && !req._gotParams.has(param)) {
|
|
356
|
+
req._gotParams.add(param);
|
|
357
|
+
const pcs = this.#paramCallbacks.get(param);
|
|
358
|
+
for(let i = 0; i < pcs.length; i++) {
|
|
359
|
+
const fn = pcs[i];
|
|
360
|
+
await new Promise(resolveRoute => {
|
|
361
|
+
const next = (thingamabob) => {
|
|
362
|
+
if(thingamabob) {
|
|
363
|
+
if(thingamabob === 'route') {
|
|
364
|
+
return resolve('route');
|
|
365
|
+
} else {
|
|
366
|
+
this.#handleError(thingamabob, req, res);
|
|
367
|
+
return resolve(false);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return resolveRoute();
|
|
371
|
+
};
|
|
372
|
+
req.next = next;
|
|
373
|
+
fn(req, res, next, req.params[param], param);
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
resolve(true)
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
return true;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
param(name, fn) {
|
|
386
|
+
if(typeof name === 'function') {
|
|
387
|
+
deprecated('app.param(callback)', 'app.param(name, callback)', true);
|
|
388
|
+
this.#paramFunction = name;
|
|
389
|
+
} else {
|
|
390
|
+
if(this.#paramFunction) {
|
|
391
|
+
if(!this.#paramCallbacks.has(name)) {
|
|
392
|
+
this.#paramCallbacks.set(name, []);
|
|
393
|
+
}
|
|
394
|
+
this.#paramCallbacks.get(name).push(this.#paramFunction(name, fn));
|
|
395
|
+
} else {
|
|
396
|
+
let names = Array.isArray(name) ? name : [name];
|
|
397
|
+
for(let name of names) {
|
|
398
|
+
if(!this.#paramCallbacks.has(name)) {
|
|
399
|
+
this.#paramCallbacks.set(name, []);
|
|
400
|
+
}
|
|
401
|
+
this.#paramCallbacks.get(name).push(fn);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
async _routeRequest(req, res, startIndex = 0, routes = this._routes, skipCheck = false, skipUntil) {
|
|
408
|
+
return new Promise(async (resolve) => {
|
|
409
|
+
let routeIndex = skipCheck ? startIndex : findIndexStartingFrom(routes, r => (r.all || r.method === req.method || (r.gettable && req.method === 'HEAD')) && this.#pathMatches(r, req), startIndex);
|
|
410
|
+
const route = routes[routeIndex];
|
|
411
|
+
if(!route) {
|
|
412
|
+
if(!skipCheck) {
|
|
413
|
+
// on normal unoptimized routes, if theres no match then there is no route
|
|
414
|
+
return resolve(false);
|
|
415
|
+
}
|
|
416
|
+
// on optimized routes, there can be more routes, so we have to use unoptimized routing and skip until we find route we stopped at
|
|
417
|
+
return resolve(this._routeRequest(req, res, 0, this._routes, false, skipUntil));
|
|
418
|
+
}
|
|
419
|
+
let callbackindex = 0;
|
|
420
|
+
let continueRoute = await this.#preprocessRequest(req, res, route);
|
|
421
|
+
if(route.use) {
|
|
422
|
+
req._stack.push(route.path);
|
|
423
|
+
req._opPath =
|
|
424
|
+
req.path.replace(this.getFullMountpath(req), '') +
|
|
425
|
+
(req.endsWithSlash && req.path !== '/' && this.get('strict routing') ? '/' : '');
|
|
426
|
+
req.url = req._opPath + req.urlQuery;
|
|
427
|
+
if(req.url === '') req.url = '/';
|
|
428
|
+
}
|
|
429
|
+
const next = async (thingamabob) => {
|
|
430
|
+
if(thingamabob) {
|
|
431
|
+
if(thingamabob === 'route' || thingamabob === 'skipPop') {
|
|
432
|
+
if(route.use && thingamabob !== 'skipPop') {
|
|
433
|
+
req._stack.pop();
|
|
434
|
+
req._opPath =
|
|
435
|
+
(req._stack.length > 0 ? req.path.replace(this.getFullMountpath(req), '') : req.path) +
|
|
436
|
+
(req.endsWithSlash && req.path !== '/' && this.get('strict routing') ? '/' : '');
|
|
437
|
+
req.url = req._opPath + req.urlQuery;
|
|
438
|
+
if(req.url === '') req.url = '/';
|
|
439
|
+
if(req.app.parent && route.callback.constructor.name === 'Application') {
|
|
440
|
+
req.app = req.app.parent;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
return resolve(this._routeRequest(req, res, routeIndex + 1, routes, skipCheck, skipUntil));
|
|
444
|
+
} else {
|
|
445
|
+
this.#handleError(thingamabob, req, res);
|
|
446
|
+
return resolve(true);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
const callback = route.callbacks[callbackindex++];
|
|
450
|
+
if(!callback) {
|
|
451
|
+
return next('route');
|
|
452
|
+
}
|
|
453
|
+
if(callback instanceof Router) {
|
|
454
|
+
if(callback.constructor.name === 'Application') {
|
|
455
|
+
req.app = callback;
|
|
456
|
+
}
|
|
457
|
+
if(callback.settings.mergeParams) {
|
|
458
|
+
req._paramStack.push(req.params);
|
|
459
|
+
}
|
|
460
|
+
const routed = await callback._routeRequest(req, res, 0);
|
|
461
|
+
if(routed) return resolve(true);
|
|
462
|
+
next();
|
|
463
|
+
} else {
|
|
464
|
+
try {
|
|
465
|
+
// skipping routes we already went through via optimized path
|
|
466
|
+
if(!skipCheck && skipUntil && skipUntil.routeKey >= route.routeKey) {
|
|
467
|
+
return next();
|
|
468
|
+
}
|
|
469
|
+
const out = callback(req, res, next);
|
|
470
|
+
if(out instanceof Promise) {
|
|
471
|
+
out.catch(err => {
|
|
472
|
+
throw err;
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
} catch(err) {
|
|
476
|
+
this.#handleError(err, req, res);
|
|
477
|
+
return resolve(true);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
req.next = next;
|
|
482
|
+
if(continueRoute === 'route') {
|
|
483
|
+
next('route');
|
|
484
|
+
} else if(continueRoute) {
|
|
485
|
+
next();
|
|
486
|
+
} else {
|
|
487
|
+
resolve(true);
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
use(path, ...callbacks) {
|
|
493
|
+
if(typeof path === 'function' || path instanceof Router || (Array.isArray(path) && path.every(p => typeof p === 'function' || p instanceof Router))) {
|
|
494
|
+
if(callbacks.length === 0 && typeof path === 'function' && path.length === 4) {
|
|
495
|
+
this.errorRoute = path;
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
callbacks.unshift(path);
|
|
499
|
+
path = '';
|
|
500
|
+
}
|
|
501
|
+
if(path === '/') {
|
|
502
|
+
path = '';
|
|
503
|
+
}
|
|
504
|
+
for(let callback of callbacks) {
|
|
505
|
+
if(callback instanceof Router) {
|
|
506
|
+
callback.mountpath = path;
|
|
507
|
+
callback.parent = this;
|
|
508
|
+
callback.emit('mount', this);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
this.#createRoute('USE', path, this, ...callbacks);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
route(path) {
|
|
515
|
+
let fns = {};
|
|
516
|
+
for(let method of methods) {
|
|
517
|
+
fns[method] = (...callbacks) => {
|
|
518
|
+
return this.#createRoute(method.toUpperCase(), path, fns, ...callbacks);
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
fns.get = (...callbacks) => {
|
|
522
|
+
return this.#createRoute('GET', path, fns, ...callbacks);
|
|
523
|
+
};
|
|
524
|
+
return fns;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
_generateErrorPage(err, checkEnv = false) {
|
|
528
|
+
if(checkEnv && this.get('env') === 'production') {
|
|
529
|
+
err = 'Internal Server Error';
|
|
530
|
+
}
|
|
531
|
+
return `<!DOCTYPE html>\n` +
|
|
532
|
+
`<html lang="en">\n` +
|
|
533
|
+
`<head>\n` +
|
|
534
|
+
`<meta charset="utf-8">\n` +
|
|
535
|
+
`<title>Error</title>\n` +
|
|
536
|
+
`</head>\n` +
|
|
537
|
+
`<body>\n` +
|
|
538
|
+
`<pre>${err?.stack ?? err}</pre>\n` +
|
|
539
|
+
`</body>\n` +
|
|
540
|
+
`</html>\n`;
|
|
541
|
+
}
|
|
542
542
|
}
|