react-concurrent-router
Version:
Performant routing embracing React concurrent UI patterns
1,466 lines (1,313 loc) • 46.3 kB
JavaScript
(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;
}));