UNPKG

react-concurrent-router

Version:

Performant routing embracing React concurrent UI patterns

1,466 lines (1,313 loc) 46.3 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('react')) : typeof define === 'function' && define.amd ? define(['exports', 'react'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.ReactConcurrentRouter = {}, global.React)); })(this, (function (exports, React) { 'use strict'; function _extends$1() { return _extends$1 = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends$1.apply(null, arguments); } function _objectWithoutPropertiesLoose(r, e) { if (null == r) return {}; var t = {}; for (var n in r) if ({}.hasOwnProperty.call(r, n)) { if (-1 !== e.indexOf(n)) continue; t[n] = r[n]; } return t; } function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } /** * Actions represent the type of change to a location value. * * @see https://github.com/remix-run/history/tree/main/docs/api-reference.md#action */ var Action; (function (Action) { /** * A POP indicates a change to an arbitrary index in the history stack, such * as a back or forward navigation. It does not describe the direction of the * navigation, only that the current index changed. * * Note: This is the default action for newly created history objects. */ Action["Pop"] = "POP"; /** * A PUSH indicates a new entry being added to the history stack, such as when * a link is clicked and a new page loads. When this happens, all subsequent * entries in the stack are lost. */ Action["Push"] = "PUSH"; /** * A REPLACE indicates the entry at the current index in the history stack * being replaced by a new one. */ Action["Replace"] = "REPLACE"; })(Action || (Action = {})); var readOnly = process.env.NODE_ENV !== "production" ? function (obj) { return Object.freeze(obj); } : function (obj) { return obj; }; function warning(cond, message) { if (!cond) { // eslint-disable-next-line no-console if (typeof console !== 'undefined') console.warn(message); try { // Welcome to debugging history! // // This error is thrown as a convenience so you can more easily // find the source for a warning that appears in the console by // enabling "pause on exceptions" in your JavaScript debugger. throw new Error(message); // eslint-disable-next-line no-empty } catch (e) {} } } var BeforeUnloadEventType = 'beforeunload'; var HashChangeEventType = 'hashchange'; var PopStateEventType = 'popstate'; /** * Browser history stores the location in regular URLs. This is the standard for * most web apps, but it requires some configuration on the server to ensure you * serve the same app at multiple URLs. * * @see https://github.com/remix-run/history/tree/main/docs/api-reference.md#createbrowserhistory */ function createBrowserHistory(options) { if (options === void 0) { options = {}; } var _options = options, _options$window = _options.window, window = _options$window === void 0 ? document.defaultView : _options$window; var globalHistory = window.history; function getIndexAndLocation() { var _window$location = window.location, pathname = _window$location.pathname, search = _window$location.search, hash = _window$location.hash; var state = globalHistory.state || {}; return [state.idx, readOnly({ pathname: pathname, search: search, hash: hash, state: state.usr || null, key: state.key || 'default' })]; } var blockedPopTx = null; function handlePop() { if (blockedPopTx) { blockers.call(blockedPopTx); blockedPopTx = null; } else { var nextAction = Action.Pop; var _getIndexAndLocation = getIndexAndLocation(), nextIndex = _getIndexAndLocation[0], nextLocation = _getIndexAndLocation[1]; if (blockers.length) { if (nextIndex != null) { var delta = index - nextIndex; if (delta) { // Revert the POP blockedPopTx = { action: nextAction, location: nextLocation, retry: function retry() { go(delta * -1); } }; go(delta); } } else { // Trying to POP to a location with no index. We did not create // this location, so we can't effectively block the navigation. process.env.NODE_ENV !== "production" ? warning(false, // TODO: Write up a doc that explains our blocking strategy in // detail and link to it here so people can understand better what // is going on and how to avoid it. "You are trying to block a POP navigation to a location that was not " + "created by the history library. The block will fail silently in " + "production, but in general you should do all navigation with the " + "history library (instead of using window.history.pushState directly) " + "to avoid this situation.") : void 0; } } else { applyTx(nextAction); } } } window.addEventListener(PopStateEventType, handlePop); var action = Action.Pop; var _getIndexAndLocation2 = getIndexAndLocation(), index = _getIndexAndLocation2[0], location = _getIndexAndLocation2[1]; var listeners = createEvents(); var blockers = createEvents(); if (index == null) { index = 0; globalHistory.replaceState(_extends({}, globalHistory.state, { idx: index }), ''); } function createHref(to) { return typeof to === 'string' ? to : createPath(to); } // state defaults to `null` because `window.history.state` does function getNextLocation(to, state) { if (state === void 0) { state = null; } return readOnly(_extends({ pathname: location.pathname, hash: '', search: '' }, typeof to === 'string' ? parsePath(to) : to, { state: state, key: createKey() })); } function getHistoryStateAndUrl(nextLocation, index) { return [{ usr: nextLocation.state, key: nextLocation.key, idx: index }, createHref(nextLocation)]; } function allowTx(action, location, retry) { return !blockers.length || (blockers.call({ action: action, location: location, retry: retry }), false); } function applyTx(nextAction) { action = nextAction; var _getIndexAndLocation3 = getIndexAndLocation(); index = _getIndexAndLocation3[0]; location = _getIndexAndLocation3[1]; listeners.call({ action: action, location: location }); } function push(to, state) { var nextAction = Action.Push; var nextLocation = getNextLocation(to, state); function retry() { push(to, state); } if (allowTx(nextAction, nextLocation, retry)) { var _getHistoryStateAndUr = getHistoryStateAndUrl(nextLocation, index + 1), historyState = _getHistoryStateAndUr[0], url = _getHistoryStateAndUr[1]; // TODO: Support forced reloading // try...catch because iOS limits us to 100 pushState calls :/ try { globalHistory.pushState(historyState, '', url); } catch (error) { // They are going to lose state here, but there is no real // way to warn them about it since the page will refresh... window.location.assign(url); } applyTx(nextAction); } } function replace(to, state) { var nextAction = Action.Replace; var nextLocation = getNextLocation(to, state); function retry() { replace(to, state); } if (allowTx(nextAction, nextLocation, retry)) { var _getHistoryStateAndUr2 = getHistoryStateAndUrl(nextLocation, index), historyState = _getHistoryStateAndUr2[0], url = _getHistoryStateAndUr2[1]; // TODO: Support forced reloading globalHistory.replaceState(historyState, '', url); applyTx(nextAction); } } function go(delta) { globalHistory.go(delta); } var history = { get action() { return action; }, get location() { return location; }, createHref: createHref, push: push, replace: replace, go: go, back: function back() { go(-1); }, forward: function forward() { go(1); }, listen: function listen(listener) { return listeners.push(listener); }, block: function block(blocker) { var unblock = blockers.push(blocker); if (blockers.length === 1) { window.addEventListener(BeforeUnloadEventType, promptBeforeUnload); } return function () { unblock(); // Remove the beforeunload listener so the document may // still be salvageable in the pagehide event. // See https://html.spec.whatwg.org/#unloading-documents if (!blockers.length) { window.removeEventListener(BeforeUnloadEventType, promptBeforeUnload); } }; } }; return history; } /** * Hash history stores the location in window.location.hash. This makes it ideal * for situations where you don't want to send the location to the server for * some reason, either because you do cannot configure it or the URL space is * reserved for something else. * * @see https://github.com/remix-run/history/tree/main/docs/api-reference.md#createhashhistory */ function createHashHistory(options) { if (options === void 0) { options = {}; } var _options2 = options, _options2$window = _options2.window, window = _options2$window === void 0 ? document.defaultView : _options2$window; var globalHistory = window.history; function getIndexAndLocation() { var _parsePath = parsePath(window.location.hash.substr(1)), _parsePath$pathname = _parsePath.pathname, pathname = _parsePath$pathname === void 0 ? '/' : _parsePath$pathname, _parsePath$search = _parsePath.search, search = _parsePath$search === void 0 ? '' : _parsePath$search, _parsePath$hash = _parsePath.hash, hash = _parsePath$hash === void 0 ? '' : _parsePath$hash; var state = globalHistory.state || {}; return [state.idx, readOnly({ pathname: pathname, search: search, hash: hash, state: state.usr || null, key: state.key || 'default' })]; } var blockedPopTx = null; function handlePop() { if (blockedPopTx) { blockers.call(blockedPopTx); blockedPopTx = null; } else { var nextAction = Action.Pop; var _getIndexAndLocation4 = getIndexAndLocation(), nextIndex = _getIndexAndLocation4[0], nextLocation = _getIndexAndLocation4[1]; if (blockers.length) { if (nextIndex != null) { var delta = index - nextIndex; if (delta) { // Revert the POP blockedPopTx = { action: nextAction, location: nextLocation, retry: function retry() { go(delta * -1); } }; go(delta); } } else { // Trying to POP to a location with no index. We did not create // this location, so we can't effectively block the navigation. process.env.NODE_ENV !== "production" ? warning(false, // TODO: Write up a doc that explains our blocking strategy in // detail and link to it here so people can understand better // what is going on and how to avoid it. "You are trying to block a POP navigation to a location that was not " + "created by the history library. The block will fail silently in " + "production, but in general you should do all navigation with the " + "history library (instead of using window.history.pushState directly) " + "to avoid this situation.") : void 0; } } else { applyTx(nextAction); } } } window.addEventListener(PopStateEventType, handlePop); // popstate does not fire on hashchange in IE 11 and old (trident) Edge // https://developer.mozilla.org/de/docs/Web/API/Window/popstate_event window.addEventListener(HashChangeEventType, function () { var _getIndexAndLocation5 = getIndexAndLocation(), nextLocation = _getIndexAndLocation5[1]; // Ignore extraneous hashchange events. if (createPath(nextLocation) !== createPath(location)) { handlePop(); } }); var action = Action.Pop; var _getIndexAndLocation6 = getIndexAndLocation(), index = _getIndexAndLocation6[0], location = _getIndexAndLocation6[1]; var listeners = createEvents(); var blockers = createEvents(); if (index == null) { index = 0; globalHistory.replaceState(_extends({}, globalHistory.state, { idx: index }), ''); } function getBaseHref() { var base = document.querySelector('base'); var href = ''; if (base && base.getAttribute('href')) { var url = window.location.href; var hashIndex = url.indexOf('#'); href = hashIndex === -1 ? url : url.slice(0, hashIndex); } return href; } function createHref(to) { return getBaseHref() + '#' + (typeof to === 'string' ? to : createPath(to)); } function getNextLocation(to, state) { if (state === void 0) { state = null; } return readOnly(_extends({ pathname: location.pathname, hash: '', search: '' }, typeof to === 'string' ? parsePath(to) : to, { state: state, key: createKey() })); } function getHistoryStateAndUrl(nextLocation, index) { return [{ usr: nextLocation.state, key: nextLocation.key, idx: index }, createHref(nextLocation)]; } function allowTx(action, location, retry) { return !blockers.length || (blockers.call({ action: action, location: location, retry: retry }), false); } function applyTx(nextAction) { action = nextAction; var _getIndexAndLocation7 = getIndexAndLocation(); index = _getIndexAndLocation7[0]; location = _getIndexAndLocation7[1]; listeners.call({ action: action, location: location }); } function push(to, state) { var nextAction = Action.Push; var nextLocation = getNextLocation(to, state); function retry() { push(to, state); } process.env.NODE_ENV !== "production" ? warning(nextLocation.pathname.charAt(0) === '/', "Relative pathnames are not supported in hash history.push(" + JSON.stringify(to) + ")") : void 0; if (allowTx(nextAction, nextLocation, retry)) { var _getHistoryStateAndUr3 = getHistoryStateAndUrl(nextLocation, index + 1), historyState = _getHistoryStateAndUr3[0], url = _getHistoryStateAndUr3[1]; // TODO: Support forced reloading // try...catch because iOS limits us to 100 pushState calls :/ try { globalHistory.pushState(historyState, '', url); } catch (error) { // They are going to lose state here, but there is no real // way to warn them about it since the page will refresh... window.location.assign(url); } applyTx(nextAction); } } function replace(to, state) { var nextAction = Action.Replace; var nextLocation = getNextLocation(to, state); function retry() { replace(to, state); } process.env.NODE_ENV !== "production" ? warning(nextLocation.pathname.charAt(0) === '/', "Relative pathnames are not supported in hash history.replace(" + JSON.stringify(to) + ")") : void 0; if (allowTx(nextAction, nextLocation, retry)) { var _getHistoryStateAndUr4 = getHistoryStateAndUrl(nextLocation, index), historyState = _getHistoryStateAndUr4[0], url = _getHistoryStateAndUr4[1]; // TODO: Support forced reloading globalHistory.replaceState(historyState, '', url); applyTx(nextAction); } } function go(delta) { globalHistory.go(delta); } var history = { get action() { return action; }, get location() { return location; }, createHref: createHref, push: push, replace: replace, go: go, back: function back() { go(-1); }, forward: function forward() { go(1); }, listen: function listen(listener) { return listeners.push(listener); }, block: function block(blocker) { var unblock = blockers.push(blocker); if (blockers.length === 1) { window.addEventListener(BeforeUnloadEventType, promptBeforeUnload); } return function () { unblock(); // Remove the beforeunload listener so the document may // still be salvageable in the pagehide event. // See https://html.spec.whatwg.org/#unloading-documents if (!blockers.length) { window.removeEventListener(BeforeUnloadEventType, promptBeforeUnload); } }; } }; return history; } /** * Memory history stores the current location in memory. It is designed for use * in stateful non-browser environments like tests and React Native. * * @see https://github.com/remix-run/history/tree/main/docs/api-reference.md#creatememoryhistory */ function createMemoryHistory(options) { if (options === void 0) { options = {}; } var _options3 = options, _options3$initialEntr = _options3.initialEntries, initialEntries = _options3$initialEntr === void 0 ? ['/'] : _options3$initialEntr, initialIndex = _options3.initialIndex; var entries = initialEntries.map(function (entry) { var location = readOnly(_extends({ pathname: '/', search: '', hash: '', state: null, key: createKey() }, typeof entry === 'string' ? parsePath(entry) : entry)); process.env.NODE_ENV !== "production" ? warning(location.pathname.charAt(0) === '/', "Relative pathnames are not supported in createMemoryHistory({ initialEntries }) (invalid entry: " + JSON.stringify(entry) + ")") : void 0; return location; }); var index = clamp(initialIndex == null ? entries.length - 1 : initialIndex, 0, entries.length - 1); var action = Action.Pop; var location = entries[index]; var listeners = createEvents(); var blockers = createEvents(); function createHref(to) { return typeof to === 'string' ? to : createPath(to); } function getNextLocation(to, state) { if (state === void 0) { state = null; } return readOnly(_extends({ pathname: location.pathname, search: '', hash: '' }, typeof to === 'string' ? parsePath(to) : to, { state: state, key: createKey() })); } function allowTx(action, location, retry) { return !blockers.length || (blockers.call({ action: action, location: location, retry: retry }), false); } function applyTx(nextAction, nextLocation) { action = nextAction; location = nextLocation; listeners.call({ action: action, location: location }); } function push(to, state) { var nextAction = Action.Push; var nextLocation = getNextLocation(to, state); function retry() { push(to, state); } process.env.NODE_ENV !== "production" ? warning(location.pathname.charAt(0) === '/', "Relative pathnames are not supported in memory history.push(" + JSON.stringify(to) + ")") : void 0; if (allowTx(nextAction, nextLocation, retry)) { index += 1; entries.splice(index, entries.length, nextLocation); applyTx(nextAction, nextLocation); } } function replace(to, state) { var nextAction = Action.Replace; var nextLocation = getNextLocation(to, state); function retry() { replace(to, state); } process.env.NODE_ENV !== "production" ? warning(location.pathname.charAt(0) === '/', "Relative pathnames are not supported in memory history.replace(" + JSON.stringify(to) + ")") : void 0; if (allowTx(nextAction, nextLocation, retry)) { entries[index] = nextLocation; applyTx(nextAction, nextLocation); } } function go(delta) { var nextIndex = clamp(index + delta, 0, entries.length - 1); var nextAction = Action.Pop; var nextLocation = entries[nextIndex]; function retry() { go(delta); } if (allowTx(nextAction, nextLocation, retry)) { index = nextIndex; applyTx(nextAction, nextLocation); } } var history = { get index() { return index; }, get action() { return action; }, get location() { return location; }, createHref: createHref, push: push, replace: replace, go: go, back: function back() { go(-1); }, forward: function forward() { go(1); }, listen: function listen(listener) { return listeners.push(listener); }, block: function block(blocker) { return blockers.push(blocker); } }; return history; } //////////////////////////////////////////////////////////////////////////////// // UTILS //////////////////////////////////////////////////////////////////////////////// function clamp(n, lowerBound, upperBound) { return Math.min(Math.max(n, lowerBound), upperBound); } function promptBeforeUnload(event) { // Cancel the event. event.preventDefault(); // Chrome (and legacy IE) requires returnValue to be set. event.returnValue = ''; } function createEvents() { var handlers = []; return { get length() { return handlers.length; }, push: function push(fn) { handlers.push(fn); return function () { handlers = handlers.filter(function (handler) { return handler !== fn; }); }; }, call: function call(arg) { handlers.forEach(function (fn) { return fn && fn(arg); }); } }; } function createKey() { return Math.random().toString(36).substr(2, 8); } /** * Creates a string URL path from the given pathname, search, and hash components. * * @see https://github.com/remix-run/history/tree/main/docs/api-reference.md#createpath */ function createPath(_ref) { var _ref$pathname = _ref.pathname, pathname = _ref$pathname === void 0 ? '/' : _ref$pathname, _ref$search = _ref.search, search = _ref$search === void 0 ? '' : _ref$search, _ref$hash = _ref.hash, hash = _ref$hash === void 0 ? '' : _ref$hash; if (search && search !== '?') pathname += search.charAt(0) === '?' ? search : '?' + search; if (hash && hash !== '#') pathname += hash.charAt(0) === '#' ? hash : '#' + hash; return pathname; } /** * Parses a string URL path into its separate pathname, search, and hash components. * * @see https://github.com/remix-run/history/tree/main/docs/api-reference.md#parsepath */ function parsePath(path) { var parsedPath = {}; if (path) { var hashIndex = path.indexOf('#'); if (hashIndex >= 0) { parsedPath.hash = path.substr(hashIndex); path = path.substr(0, hashIndex); } var searchIndex = path.indexOf('?'); if (searchIndex >= 0) { parsedPath.search = path.substr(searchIndex); path = path.substr(0, searchIndex); } if (path) { parsedPath.pathname = path; } } return parsedPath; } class SuspendableResource { constructor(loader, isModule = false) { this.load = () => { if (this._result) return this._result; if (this._promise) return this._promise; this._promise = this._loader().then(result => { const returnValue = this._isModule ? result.default || result : result; this._result = returnValue; return this._result; }).catch(error => { this._error = error; }); return this._promise; }; this.isLoaded = () => Boolean(this._result); this.read = () => { if (this._result) return this._result; if (this._error) throw this._error; if (this._promise) throw this._promise; throw this.load(); }; this._isModule = isModule; this._loader = loader; this._promise = null; this._result = null; this._error = null; } } const _excluded$4 = ["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$4); 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$1({}, parentProps, routeProps, !isGroupRoute && { component: new SuspendableResource(routeProps.component, true) }); if (!isGroupRoute) routesMap.set(canonicalPath, computedRoute); if (children && Array.isArray(children)) { routesIterator(children, _extends$1({}, 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$1({}, 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$1(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; }; const createRouter = ({ assistPrefetch = false, awaitComponent = false, awaitPrefetch = false, history, routes }) => { const routesMap = routesToMap(routes); const entryMatch = matchRoutes(routesMap, history.location); let currentEntry = prepareMatch(entryMatch, assistPrefetch, awaitPrefetch); if (!locationsMatch(entryMatch.location, history.location, true)) { history.replace(entryMatch.location); } let nextId = 0; const subscribers = new Map(); history.listen(({ location, action }) => { if (locationsMatch(currentEntry.location, location, true)) return; const skipRender = location.state && location.state.skipRender && action !== 'POP'; const match = matchRoutes(routesMap, location); const nextEntry = skipRender ? _extends$1({}, currentEntry, { location: match.location, params: match.params, skipRender }) : prepareMatch(match, assistPrefetch, awaitPrefetch); if (!locationsMatch(match.location, location, true)) { return history.replace(match.location); } currentEntry = nextEntry; subscribers.forEach(callback => callback(nextEntry)); }); return { assistPrefetch, awaitComponent, history, isActive: (path, { exact } = {}) => locationsMatch(history.location, path, exact), get: () => currentEntry, preloadCode: (path, { ignoreRedirectRules } = {}) => { const { route } = matchRoutes(routesMap, path, ignoreRedirectRules); route.component.load(); }, warmRoute: (path, { ignoreRedirectRules } = {}) => { const match = matchRoutes(routesMap, path, ignoreRedirectRules); prepareMatch(match, assistPrefetch, awaitPrefetch); }, subscribe: callback => { const id = nextId++; const dispose = () => { subscribers.delete(id); }; subscribers.set(id, callback); return dispose; } }; }; const _excluded$3 = ["window"]; const createBrowserRouter = _ref => { let { window } = _ref, routerConfig = _objectWithoutPropertiesLoose(_ref, _excluded$3); return createRouter(_extends$1({}, routerConfig, { history: createBrowserHistory({ window }) })); }; const _excluded$2 = ["window"]; const createHashRouter = _ref => { let { window } = _ref, routerConfig = _objectWithoutPropertiesLoose(_ref, _excluded$2); return createRouter(_extends$1({}, routerConfig, { history: createHashHistory({ window }) })); }; const _excluded$1 = ["initialEntries", "initialIndex"]; const createMemoryRouter = _ref => { let { initialEntries, initialIndex } = _ref, routerConfig = _objectWithoutPropertiesLoose(_ref, _excluded$1); return createRouter(_extends$1({}, routerConfig, { history: createMemoryHistory({ initialEntries, initialIndex }) })); }; const RouterContext = React.createContext(null); const useBeforeRouteLeave = ({ toggle = true, unload = true, message = '' }) => { const { history: { block } } = React.useContext(RouterContext); let unblock; const handleBeforeunload = React.useCallback(event => { event.preventDefault(); event.returnValue = ''; }, []); const register = React.useCallback(() => { unblock = block(message); if (unload) { window.addEventListener('beforeunload', handleBeforeunload); } }, [block]); const cleanup = React.useCallback(() => { unblock(); if (unload) { window.removeEventListener('beforeunload', handleBeforeunload); } }, [unblock]); React.useEffect(() => { if (toggle) register(); return () => { if (toggle) cleanup(); }; }, [toggle, register, cleanup]); }; const useHistory = () => { const { history: { length, location, action, index, entries }, subscribe } = React.useContext(RouterContext); const [lastUpdate, setLastUpdate] = React.useState(new Date().getTime()); React.useEffect(() => { const dispose = subscribe(async () => { setTimeout(() => setLastUpdate(new Date().getTime()), 1); }); return () => dispose(); }, []); return { length, location, action, index, entries }; }; const useNavigation = () => { const { history: { push, replace, go, back, forward } } = React.useContext(RouterContext); return { push, replace, go, goBack: back, goForward: forward }; }; const useParams = () => { const { get, subscribe } = React.useContext(RouterContext); const [params, setParams] = React.useState(get().params); React.useEffect(() => { const dispose = subscribe(async nextEntry => { setTimeout(() => setParams(nextEntry.params), 1); }); return () => dispose(); }, []); return params; }; const useRouter = () => { const { isActive, preloadCode, warmRoute } = React.useContext(RouterContext); return { isActive, preloadCode, warmRoute }; }; const useSearchParams = () => { const { get, history, subscribe } = React.useContext(RouterContext); const [searchParams, setSearchParams] = React.useState(paramsStringToObject(get().location.search)); const handleSetSearchParams = React.useCallback((newParams, { replace = false } = {}) => { const { location } = get(); const currentSearchParams = typeof newParams === 'function' && paramsStringToObject(location.search); const newSearchParams = typeof newParams === 'function' ? newParams(currentSearchParams) : newParams; history[replace ? 'replace' : 'push']({ pathname: location.pathname, search: sortAndStringifyRequestParams(newSearchParams) }, _extends$1({}, location.state, replace && { skipRender: true })); }, []); React.useEffect(() => { const dispose = subscribe(async nextEntry => { const newSearchParams = paramsStringToObject(nextEntry.location.search); setTimeout(() => setSearchParams(newSearchParams), 1); }); return () => dispose(); }, []); return [searchParams, handleSetSearchParams]; }; const RouterProvider = ({ children, router }) => React.createElement(RouterContext.Provider, { value: router, children: children }); const RouteRenderer = ({ pendingIndicator }) => { const { assistPrefetch, awaitComponent, get, subscribe } = React.useContext(RouterContext); const computeInitialEntry = React.useCallback(entry => { if (!assistPrefetch || !entry.prefetched) return entry; const prefetched = {}; for (const [property, value] of entry.prefetched.entries()) { prefetched[property] = value.data; } return _extends$1({}, entry, { prefetched }); }, []); const [isPendingEntry, setIsPendingEntry] = React.useState(false); const [routeEntry, setRouteEntry] = React.useState(computeInitialEntry(get())); const Component = React.useMemo(() => routeEntry.component.read(), [routeEntry]); const processFetchEntities = React.useCallback(pendingEntry => { if (!pendingEntry.assistedPrefetch) { return { prefetched: pendingEntry.prefetched, toBePrefetched: [] }; } const prefetched = {}; const toBePrefetched = []; for (const [property, value] of pendingEntry.prefetched.entries()) { if (value.defer === false && value.data.isLoaded() === false) { toBePrefetched.push({ key: property, data: value.data }); } else prefetched[property] = value.data; } return { prefetched, toBePrefetched }; }, []); React.useEffect(() => { const dispose = subscribe(async nextEntry => { if (nextEntry.skipRender) return; const { prefetched, toBePrefetched } = processFetchEntities(nextEntry); const shouldUpdatePendingIndicator = Boolean(pendingIndicator && (awaitComponent && !nextEntry.component.isLoaded() || nextEntry.assistedPrefetch && toBePrefetched.length)); if (shouldUpdatePendingIndicator) setIsPendingEntry(true); if (awaitComponent) await nextEntry.component.load(); const newlyPrefetched = toBePrefetched.length ? await toBePrefetched.reduce(async (newlyPrefetched, { key, data }) => { await data.load(); return _extends$1({}, newlyPrefetched, { [key]: data }); }, {}) : {}; const routeEntry = _extends$1({}, nextEntry, { prefetched: _extends$1({}, prefetched, newlyPrefetched) }); setRouteEntry(routeEntry); if (shouldUpdatePendingIndicator) setIsPendingEntry(false); }); return () => dispose(); }, [assistPrefetch, awaitComponent, processFetchEntities, pendingIndicator, subscribe]); const locationKey = routeEntry.location ? routeEntry.location.pathname + routeEntry.location.search + routeEntry.location.hash : 'default'; return React.createElement(React.Fragment, { key: locationKey }, isPendingEntry && pendingIndicator ? pendingIndicator : null, React.createElement(Component, { key: window.location.href, params: routeEntry.params, prefetched: routeEntry.prefetched })); }; const _excluded = ["activeClassName", "exact", "target", "to", "onClick"]; const shouldNavigate = event => !event.defaultPrevented && event.button === 0 && (!event.target.target || event.target.target === '_self') && !(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey); const Link = React.forwardRef((_ref, ref) => { let { activeClassName, exact, target, to, onClick } = _ref, props = _objectWithoutPropertiesLoose(_ref, _excluded); const { isActive, preloadCode, warmRoute, history } = React.useContext(RouterContext); const toIsActive = isActive(to, { exact }); const handleClick = React.useCallback(event => { if (onClick) onClick(event); if (!shouldNavigate(event)) return; event.preventDefault(); const navigationMethod = isActive(to, { exact: true }) ? 'replace' : 'push'; history[navigationMethod](to); }, [onClick, isActive, to, history]); const handlePreloadCode = React.useCallback(() => { preloadCode(to); }, [preloadCode, to]); const handleWarmRoute = React.useCallback(({ type, key, code, keyCode }) => { if (type === 'mousedown' || type === 'keydown' && (key === 'Enter' || code === 'Enter' || code === 'NumpadEnter' || keyCode === 13)) { warmRoute(to); } }, [warmRoute, to]); const elementProps = _extends$1({}, props, { target, ref, href: to, onClick: handleClick, onMouseOver: handlePreloadCode, onFocus: handlePreloadCode, onMouseDown: handleWarmRoute, onKeyDown: handleWarmRoute }, toIsActive && { className: props.className ? `${props.className} ${activeClassName}` : activeClassName, 'aria-current': 'page' }); return React.createElement("a", elementProps); }); Link.defaultProps = { activeClassName: 'active', exact: false }; Link.displayName = 'Link'; exports.Link = Link; exports.RouteRenderer = RouteRenderer; exports.RouterProvider = RouterProvider; exports.SuspendableResource = SuspendableResource; exports.createBrowserRouter = createBrowserRouter; exports.createHashRouter = createHashRouter; exports.createMemoryRouter = createMemoryRouter; exports.useBeforeRouteLeave = useBeforeRouteLeave; exports.useHistory = useHistory; exports.useNavigation = useNavigation; exports.useParams = useParams; exports.useRouter = useRouter; exports.useSearchParams = useSearchParams; }));