universal-router
Version:
Isomorphic router for JavaScript web applications
150 lines • 5.58 kB
JavaScript
/**
* Universal Router (https://www.kriasoft.com/universal-router/)
*
* Copyright (c) 2015-present Kriasoft.
*
* This source code is licensed under the MIT license found in the
* LICENSE.txt file in the root directory of this source tree.
*/
import { match } from './path-to-regexp.js';
function decode(val) {
try {
return decodeURIComponent(val);
}
catch {
return val;
}
}
function matchRoute(route, baseUrl, options, pathname, parentParams) {
let matchResult;
let childMatches;
let childIndex = 0;
return {
next(routeToSkip) {
if (route === routeToSkip) {
return { done: true, value: false };
}
if (!matchResult) {
const rt = route;
const end = !rt.children;
if (!rt.match) {
rt.match = match(rt.path || '', { end, ...options });
}
matchResult = rt.match(pathname);
if (matchResult) {
const { path } = matchResult;
matchResult.path =
!end && path.charAt(path.length - 1) === '/' ? path.substr(1) : path;
matchResult.params = { ...parentParams, ...matchResult.params };
return {
done: false,
value: {
route,
baseUrl,
path: matchResult.path,
params: matchResult.params,
},
};
}
}
if (matchResult && route.children) {
while (childIndex < route.children.length) {
if (!childMatches) {
const childRoute = route.children[childIndex];
childRoute.parent = route;
childMatches = matchRoute(childRoute, baseUrl + matchResult.path, options, pathname.substr(matchResult.path.length), matchResult.params);
}
const childMatch = childMatches.next(routeToSkip);
if (!childMatch.done) {
return { done: false, value: childMatch.value };
}
childMatches = null;
childIndex++;
}
}
return { done: true, value: false };
},
};
}
function resolveRoute(context, params) {
if (typeof context.route.action === 'function') {
return context.route.action(context, params);
}
return undefined;
}
function isChildRoute(parentRoute, childRoute) {
let route = childRoute;
while (route) {
route = route.parent;
if (route === parentRoute) {
return true;
}
}
return false;
}
class UniversalRouter {
constructor(routes, options) {
if (!routes || typeof routes !== 'object') {
throw new TypeError('Invalid routes');
}
this.options = { decode, ...options };
this.baseUrl = this.options.baseUrl || '';
this.root = Array.isArray(routes)
? { path: '', children: routes, parent: null }
: routes;
this.root.parent = null;
}
/**
* Traverses the list of routes in the order they are defined until it finds
* the first route that matches provided URL path string and whose action function
* returns anything other than `null` or `undefined`.
*/
resolve(pathnameOrContext) {
const context = {
router: this,
...this.options.context,
...(typeof pathnameOrContext === 'string'
? { pathname: pathnameOrContext }
: pathnameOrContext),
};
const matchResult = matchRoute(this.root, this.baseUrl, this.options, context.pathname.substr(this.baseUrl.length));
const resolve = this.options.resolveRoute || resolveRoute;
let matches;
let nextMatches;
let currentContext = context;
function next(resume, parent = !matches.done && matches.value.route, prevResult) {
const routeToSkip = prevResult === null && !matches.done && matches.value.route;
matches = nextMatches || matchResult.next(routeToSkip);
nextMatches = null;
if (!resume) {
if (matches.done || !isChildRoute(parent, matches.value.route)) {
nextMatches = matches;
return Promise.resolve(null);
}
}
if (matches.done) {
const error = new Error('Route not found');
error.status = 404;
return Promise.reject(error);
}
currentContext = { ...context, ...matches.value };
return Promise.resolve(resolve(currentContext, matches.value.params)).then((result) => {
if (result !== null && result !== undefined) {
return result;
}
return next(resume, parent, result);
});
}
context['next'] = next;
return Promise.resolve()
.then(() => next(true, this.root))
.catch((error) => {
if (this.options.errorHandler) {
return this.options.errorHandler(error, currentContext);
}
throw error;
});
}
}
export default UniversalRouter;
//# sourceMappingURL=universal-router.js.map