react-concurrent-router
Version:
Performant routing embracing React concurrent UI patterns
192 lines (189 loc) • 7.71 kB
JavaScript
import _extends from '@babel/runtime/helpers/esm/extends';
import _objectWithoutPropertiesLoose from '@babel/runtime/helpers/esm/objectWithoutPropertiesLoose';
import { parsePath } from 'history';
import SuspendableResource from './SuspendableResource.js';
const _excluded = ["path", "children"],
_excluded2 = ["path"];
const lastPreparedMatch = {
pathname: '',
paramsString: '',
value: null
};
const getCanonicalPath = path => path.charAt(0) === '/' ? path : `/${path}`;
const sortAndStringifyRequestParams = params => {
const optimisedParamsArray = [];
for (const param in params) {
if (!Object.prototype.hasOwnProperty.call(params, param)) continue;
optimisedParamsArray.push({
index: optimisedParamsArray.length,
value: param
});
}
return optimisedParamsArray.sort(({
value: firstValue
}, {
value: secondValue
}) => firstValue > secondValue ? 1 : -1).reduce((identifier, element) => {
const rawParamValue = params[element.value];
const paramValue = Array.isArray(rawParamValue) ? rawParamValue.reduce((params, value, index) => {
const encodedValue = encodeURIComponent(value);
return params.concat(index >= 1 ? `&${element.value}=${encodedValue}` : encodedValue);
}, '') : encodeURIComponent(rawParamValue);
return `${identifier}${!identifier ? '?' : '&'}${element.value}=${paramValue}`;
}, '');
};
const aggregateKeyValues = (list, key, value = '') => {
const decodedValue = decodeURIComponent(value);
const keyValue = list[key] ? Array.isArray(list[key]) ? list[key].concat(decodedValue) : [list[key], decodedValue] : decodedValue;
return keyValue;
};
const paramsStringToObject = search => {
if (!search) return {};
const paramsString = search.slice(1).split('&');
return paramsString.reduce((params, current) => {
const [key, value] = current.split('=');
const keyValue = aggregateKeyValues(params, key, value);
params[key] = keyValue;
return params;
}, {});
};
const routesToMap = routes => {
const routesMap = new Map();
const routesIterator = (inputRoutes, parent = {}, groupDescendant = false) => inputRoutes.forEach(route => {
const {
path,
children
} = route,
routeProps = _objectWithoutPropertiesLoose(route, _excluded);
const {
path: parentPath = ''
} = parent,
parentProps = _objectWithoutPropertiesLoose(parent, _excluded2);
const canonicalParentPath = parentPath === '/' ? '' : parentPath;
const canonicalRoutePath = path ? getCanonicalPath(path) : '';
const canonicalPath = canonicalParentPath + canonicalRoutePath;
const isGroupRoute = !routeProps.component;
const computedRoute = _extends({}, parentProps, routeProps, !isGroupRoute && {
component: new SuspendableResource(routeProps.component, true)
});
if (!isGroupRoute) routesMap.set(canonicalPath, computedRoute);
if (children && Array.isArray(children)) {
routesIterator(children, _extends({}, isGroupRoute && routeProps || groupDescendant && parent, {
path: canonicalPath
}), isGroupRoute || groupDescendant);
}
});
routesIterator(routes);
if (process.env.NODE_ENV !== 'production' && !routesMap.has('/*')) {
console.warn(`You didn't set a wildcard (*) route to catch any unmatched path.
This is required to make sure you push users to a Not Found page
when they request a route that doesn't exist; e.g. 404.`);
}
return routesMap;
};
const pathToLocation = location => typeof location === 'string' ? parsePath(location) : location;
const locationsMatch = (left, right, exact = false) => {
const leftLocation = pathToLocation(left);
const rightLocation = pathToLocation(right);
if (leftLocation.pathname !== rightLocation.pathname) return false;
return exact ? leftLocation.search === rightLocation.search && leftLocation.hash === rightLocation.hash : true;
};
const matchRegexRoute = (referencePath, pathname) => {
const pathToMatch = getCanonicalPath(pathname);
const paramsKeys = [];
const pattern = '^(' + referencePath.replace(/[.*+\-?^$/{}()|[\]\\]/g, '\\$&').replace(/\\\*$/, '.*').replace(/:(\w+)|(.\*)/g, (_, paramKey = 'rest') => {
paramsKeys.push(paramKey);
return `([^${paramKey === 'rest' ? ':(w+)|(.*)' : '\\/'}]+)`;
}) + ')\\/?$';
const matcher = new RegExp(pattern);
const match = pathToMatch.match(matcher);
if (!match) return null;
const params = paramsKeys.reduce((collection, paramKey, index) => {
const value = match[index + 2];
const keyValue = aggregateKeyValues(collection, paramKey, value);
collection[paramKey] = keyValue;
return collection;
}, {});
return {
params
};
};
const matchRoutes = (routes, requestedMatch, ignoreRedirectRules = false) => {
const locationToMatch = pathToLocation(requestedMatch);
const {
pathname,
search
} = locationToMatch;
const params = _extends({}, paramsStringToObject(search));
let matchedRoute = routes.has(pathname) && routes.get(pathname);
if (!matchedRoute) {
for (const [path, route] of routes.entries()) {
if (path !== '/*') {
const match = matchRegexRoute(path, pathname);
if (!match) continue;
_extends(params, match.params);
}
matchedRoute = route;
break;
}
}
if (!matchedRoute) return null;
const redirectPath = !ignoreRedirectRules && matchedRoute.redirectRules && matchedRoute.redirectRules(params);
return redirectPath ? matchRoutes(routes, redirectPath) : {
route: matchedRoute,
params,
location: locationToMatch
};
};
const prepareAssistPrefetchMatch = ({
params,
location
}, prefetchToAssist, awaitPrefetch) => {
const pathnameMatch = location.pathname === lastPreparedMatch.pathname;
const paramsString = pathnameMatch && sortAndStringifyRequestParams(params);
if (pathnameMatch && paramsString === lastPreparedMatch.paramsString) {
return lastPreparedMatch.value;
}
const prefetched = new Map();
const prefetch = prefetchToAssist(params);
for (const property in prefetch) {
if (!Object.prototype.hasOwnProperty.call(prefetch, property)) {
continue;
}
const isFetchFunction = typeof prefetch[property] === 'function';
const fetchFunction = isFetchFunction ? prefetch[property] : prefetch[property].data;
const fetchResource = new SuspendableResource(fetchFunction);
fetchResource.load();
prefetched.set(property, {
defer: !isFetchFunction && prefetch[property].defer !== undefined ? prefetch[property].defer : !awaitPrefetch,
data: fetchResource
});
}
lastPreparedMatch.pathname = location.pathname;
lastPreparedMatch.paramsString = paramsString || sortAndStringifyRequestParams(params);
return prefetched;
};
const prepareMatch = (match, assistPrefetch, awaitPrefetch) => {
const {
route,
params,
location
} = match;
const prefetchToAssist = assistPrefetch && route.prefetch || route.assistedPrefetch;
const prefetched = prefetchToAssist ? prepareAssistPrefetchMatch(match, prefetchToAssist, awaitPrefetch) : route.prefetch == null ? void 0 : route.prefetch(params);
const assistedPrefetch = Boolean(prefetchToAssist && prefetched);
if (assistedPrefetch && prefetched === lastPreparedMatch.value) {
return lastPreparedMatch.value;
}
route.component.load();
const preparedMatch = {
location,
component: route.component,
params,
prefetched,
assistedPrefetch
};
if (assistedPrefetch) lastPreparedMatch.value = preparedMatch;
return preparedMatch;
};
export { paramsStringToObject as a, locationsMatch as l, matchRoutes as m, prepareMatch as p, routesToMap as r, sortAndStringifyRequestParams as s };