UNPKG

react-concurrent-router

Version:

Performant routing embracing React concurrent UI patterns

192 lines (189 loc) 7.71 kB
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 };