expo-router
Version:
Expo Router is a file-based router for React Native and web applications.
650 lines (648 loc) • 30.3 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
0 && (module.exports = {
TabActions: null,
TabRouter: null,
ensureStateHistory: null
});
function _export(target, all) {
for(var name in all)Object.defineProperty(target, name, {
enumerable: true,
get: Object.getOwnPropertyDescriptor(all, name).get
});
}
_export(exports, {
get TabActions () {
return TabActions;
},
get TabRouter () {
return TabRouter;
},
get ensureStateHistory () {
return ensureStateHistory;
}
});
function _reactfastcompare() {
const data = /*#__PURE__*/ _interop_require_default(require("react-fast-compare"));
_reactfastcompare = function() {
return data;
};
return data;
}
const _isArrayEqual = require("../core/isArrayEqual");
const _BaseRouter = require("./BaseRouter");
const _attachRouteState = require("./attachRouteState");
const _createRouteFromAction = require("./createRouteFromAction");
const _extendRouter = require("./extendRouter");
const _getBrowserHistoryForHistoryChange = require("./getBrowserHistoryForHistoryChange");
const _types = require("./types");
const _utils = require("./utils");
function _interop_require_default(obj) {
return obj && obj.__esModule ? obj : {
default: obj
};
}
const TYPE_ROUTE = 'route';
function clearFocusedPreloadedRoute(state) {
const route = state.routes[state.index];
if (!route?.isPreloaded) {
return state;
}
const { isPreloaded, ...focusedRoute } = route;
const routes = [
...state.routes
];
// Removing an optional field preserves the route's conditional params type, which TypeScript
// cannot infer through the `Route` intersection.
routes[state.index] = focusedRoute;
return {
...state,
routes
};
}
// A route that `getId` re-keys is a new route instance, so it must not keep the nested navigator
// state of the previous ID.
function withoutStaleState(route, key) {
if (key === route.key) {
return route;
}
const { state, ...routeWithoutState } = route;
return routeWithoutState;
}
const addFallbackRouteIfEmpty = (routes, routeNames, initialRouteName, mintRouteKey)=>{
if (routes.length > 0 || routeNames.length === 0) {
return routes;
}
const name = initialRouteName !== undefined && routeNames.includes(initialRouteName) ? initialRouteName : routeNames[0];
return [
{
name,
key: mintRouteKey(name)
}
];
};
const addRouteIfMissing = (routes, name, createRoute)=>{
const existingIndex = routes.findIndex((route)=>route.name === name);
if (existingIndex !== -1) {
return {
routes,
index: existingIndex
};
}
return {
routes: [
...routes,
createRoute()
],
index: routes.length
};
};
const TabActions = {
jumpTo (name, params) {
return {
type: 'JUMP_TO',
payload: {
name,
params
}
};
},
replace (name, params) {
return {
type: 'REPLACE',
payload: {
name,
params
}
};
}
};
const getRouteHistory = (routes, index, backBehavior, initialRouteName)=>{
if (routes.length === 0) {
return [];
}
const history = [
{
type: TYPE_ROUTE,
key: routes[index].key
}
];
let initialRouteIndex;
switch(backBehavior){
case 'order':
for(let i = index; i > 0; i--){
history.unshift({
type: TYPE_ROUTE,
key: routes[i - 1].key
});
}
break;
case 'firstRoute':
if (index !== 0) {
history.unshift({
type: TYPE_ROUTE,
key: routes[0].key
});
}
break;
case 'initialRoute':
initialRouteIndex = routes.findIndex((route)=>route.name === initialRouteName);
initialRouteIndex = initialRouteIndex === -1 ? 0 : initialRouteIndex;
if (index !== initialRouteIndex) {
history.unshift({
type: TYPE_ROUTE,
key: routes[initialRouteIndex].key
});
}
break;
case 'history':
case 'fullHistory':
break;
}
return history;
};
// Unvisited tabs use the same placeholder keys as their descriptors until navigation creates a route.
const getHistoryRoutes = (routes, routeNames)=>routeNames.map((name)=>routes.find((route)=>route.name === name) ?? {
name,
key: name
});
const ensureStateHistory = (state, backBehavior, initialRouteName)=>{
if (state.history != null) {
// The null check narrows the optional property, but TypeScript doesn't narrow the object type.
return state;
}
const routes = getHistoryRoutes(state.routes, state.routeNames);
const focusedRoute = state.routes[state.index];
const index = routes.findIndex((route)=>route.key === focusedRoute?.key);
// The declared routes can omit the focused route, so it can be missing from
// `routes`. Keep it in history anyway, so the current route is always the last entry.
const history = index === -1 ? focusedRoute === undefined ? [] : [
{
type: TYPE_ROUTE,
key: focusedRoute.key
}
] : getRouteHistory(routes, index, backBehavior, initialRouteName);
if (backBehavior === 'fullHistory' && focusedRoute !== undefined) {
history[history.length - 1] = {
...history[history.length - 1],
params: focusedRoute.params
};
}
return {
...state,
history
};
};
const changeIndex = (state, index, backBehavior, initialRouteName)=>{
if (state.routes.length === 0) {
return {
...state,
index: -1,
history: []
};
}
let history = state.history;
if (backBehavior === 'history' || backBehavior === 'fullHistory') {
const currentRoute = state.routes[index];
if (backBehavior === 'history') {
// Remove the existing key from the history to de-duplicate it
history = history.filter((it)=>it.type === 'route' ? it.key !== currentRoute.key : false);
} else if (backBehavior === 'fullHistory') {
const lastHistoryRouteItemIndex = history.findLastIndex((item)=>item.type === 'route');
if (currentRoute.key === history[lastHistoryRouteItemIndex]?.key) {
// For full-history, only remove if it matches the last route
// Useful for drawer, if current route was in history, then drawer state changed
// Then we only need to move the route to the front
history = [
...history.slice(0, lastHistoryRouteItemIndex),
...history.slice(lastHistoryRouteItemIndex + 1)
];
}
}
history = history.concat({
type: TYPE_ROUTE,
key: currentRoute.key,
params: backBehavior === 'fullHistory' ? currentRoute.params : undefined
});
} else {
const routes = getHistoryRoutes(state.routes, state.routeNames);
const orderedIndex = routes.findIndex((route)=>route.key === state.routes[index].key);
history = getRouteHistory(routes, orderedIndex, backBehavior, initialRouteName);
}
return {
...state,
index,
history
};
};
function tabRouterExtension({ baseRouter, nextKey, options: { initialRouteName, backBehavior = 'firstRoute' } }) {
const normalizeHistory = (state)=>{
if (backBehavior === 'history' || backBehavior === 'fullHistory') {
return ensureStateHistory(state, backBehavior, initialRouteName);
}
const normalized = ensureStateHistory({
...state,
history: undefined
}, backBehavior, initialRouteName);
const history = [
...normalized.history,
...state.history?.filter((item)=>item.type !== 'route') ?? []
];
return (0, _reactfastcompare().default)(history, state.history) ? state : {
...state,
history
};
};
const getBrowserHistoryForAction = (previous, next, action)=>{
switch(action.type){
case 'PUSH':
case 'NAVIGATE':
case 'JUMP_TO':
// Selecting another tab is a new browser visit even if tab history removes a duplicate.
// For example, Home -> Search -> Home still needs three browser entries.
if ((backBehavior === 'history' || backBehavior === 'fullHistory') && previous.routes[previous.index]?.key !== next.routes[next.index]?.key) {
return {
type: 'push'
};
}
break;
case 'GO_BACK':
break;
default:
return undefined;
}
// URL-derived state may have no history yet. Action/focus handling already fills in `next`.
// Reconstruct `previous` too, so that initialization is not counted as a new browser visit.
return (0, _getBrowserHistoryForHistoryChange.getBrowserHistoryForHistoryChange)(ensureStateHistory(previous, backBehavior, initialRouteName), next);
};
// TODO: Simplify the action handling in this router.
const router = {
normalizeState: clearFocusedPreloadedRoute,
getStateForDeclaredRoutes (state, routeNames) {
const declared = baseRouter.getStateForDeclaredRoutes(state, routeNames);
if (declared.history != null && (0, _isArrayEqual.isArrayEqual)(declared.routeNames, routeNames)) {
return declared;
}
return normalizeHistory((0, _isArrayEqual.isArrayEqual)(declared.routeNames, routeNames) ? declared : {
...declared,
routeNames
});
},
getBrowserHistoryForAction,
getBrowserHistoryForRouteFocus: (previous, next)=>getBrowserHistoryForAction(previous, next, {
type: 'JUMP_TO'
}),
getStateForRouteFocus (inputState, key) {
const state = ensureStateHistory(inputState, backBehavior, initialRouteName);
const index = state.routes.findIndex((r)=>r.key === key);
if (index === -1 || index === state.index) {
return state;
}
return changeIndex(state, index, backBehavior, initialRouteName);
},
getStateForAction (inputState, action, options) {
const { routeGetIdList } = options;
const state = ensureStateHistory(inputState, backBehavior, initialRouteName);
if (action.target && action.target !== state.key) {
return _types.RouterActionHandling.Unhandled;
}
switch(action.type){
case 'ROUTE_NAMES_CHANGED':
{
const routeNames = action.payload.routeNames;
if ((0, _isArrayEqual.isArrayEqual)(state.routeNames, routeNames)) {
return {
state: normalizeHistory(state),
affectedRouteKey: state.routes[state.index]?.key
};
}
const routes = addFallbackRouteIfEmpty(state.routes.filter((route)=>routeNames.includes(route.name)), routeNames, initialRouteName, nextKey);
if (routes.length === 0) {
return {
state: {
...state,
routeNames,
routes,
index: -1,
history: []
},
affectedRouteKey: undefined
};
}
const focusedKey = state.routes[state.index]?.key;
const focusedIndex = routes.findIndex((route)=>route.key === focusedKey);
const index = Math.max(focusedIndex, 0);
const routeKeys = routes.map((route)=>route.key);
let history = state.history.filter((item)=>item.type !== 'route' || routeKeys.includes(item.key));
if (focusedIndex === -1 && (backBehavior === 'history' || backBehavior === 'fullHistory')) {
const currentRoute = routes[index];
const nonRouteHistory = history.filter((item)=>item.type !== 'route');
let routeHistory = history.filter((item)=>item.type === 'route');
if (backBehavior === 'history') {
routeHistory = routeHistory.filter((item)=>item.key !== currentRoute.key);
} else if (routeHistory[routeHistory.length - 1]?.key === currentRoute.key) {
routeHistory = routeHistory.slice(0, -1);
}
history = [
...routeHistory,
{
type: TYPE_ROUTE,
key: currentRoute.key,
params: backBehavior === 'fullHistory' ? currentRoute.params : undefined
},
...nonRouteHistory
];
}
if (backBehavior === 'firstRoute' || backBehavior === 'initialRoute' || backBehavior === 'order') {
const orderedRoutes = getHistoryRoutes(routes, routeNames);
const orderedIndex = orderedRoutes.findIndex((route)=>route.key === routes[index].key);
history = [
...getRouteHistory(orderedRoutes, orderedIndex, backBehavior, initialRouteName),
...history.filter((item)=>item.type !== 'route')
];
} else if (!history.some((item)=>item.type === 'route')) {
history = [
...getRouteHistory(routes, index, backBehavior, initialRouteName),
...history.filter((item)=>item.type !== 'route')
];
}
return {
state: normalizeHistory({
...state,
history,
routeNames,
routes,
index
}),
affectedRouteKey: routes[index].key
};
}
case 'PUSH':
case 'REPLACE':
case 'JUMP_TO':
case 'NAVIGATE':
{
if (!state.routeNames.includes(action.payload.name)) {
return _types.RouterActionHandling.Unhandled;
}
const { routes, index } = addRouteIfMissing(state.routes, action.payload.name, ()=>{
const route = (0, _createRouteFromAction.createRouteFromAction)({
action,
key: nextKey(action.payload.name)
});
return action.type === 'NAVIGATE' && action.payload.path != null ? {
...route,
path: action.payload.path
} : route;
});
const updatedState = changeIndex({
...state,
routes: routes.map((route)=>{
if (route.name !== action.payload.name) {
return route;
}
const getId = routeGetIdList[route.name];
const currentId = getId?.({
params: route.params
});
const nextId = getId?.({
params: action.payload.params
});
// TODO(@ubax): Rewrite `history` when `getId` re-keys a route, as `PRELOAD` does with `replacedKey`.
const key = currentId === nextId ? route.key : nextKey(route.name);
let params;
if (action.type === 'NAVIGATE' && action.payload.merge && currentId === nextId) {
params = action.payload.params !== undefined ? {
...route.params,
...action.payload.params
} : route.params;
} else {
params = action.payload.params;
}
const path = action.type === 'NAVIGATE' && action.payload.path != null ? action.payload.path : route.path;
const updatedRoute = params !== route.params || path !== route.path ? {
...withoutStaleState(route, key),
key,
path,
params
} : route;
return (0, _attachRouteState.attachRouteState)(updatedRoute, action);
})
}, index, backBehavior, initialRouteName);
const result = action.type === 'REPLACE' ? removeReplacedRouteFromHistory(state, updatedState) : updatedState;
return {
state: result,
affectedRouteKey: result.routes[result.index]?.key
};
}
case 'SET_PARAMS':
case 'REPLACE_PARAMS':
{
const actionResult = baseRouter.getStateForAction(state, action, options);
if ((0, _utils.isActionHandled)(actionResult)) {
const nextState = actionResult.state;
const index = nextState.index;
if (index != null) {
const focusedRoute = nextState.routes[index];
const historyItemIndex = state.history.findLastIndex((item)=>item.key === focusedRoute.key);
let updatedHistory = state.history;
if (historyItemIndex !== -1) {
updatedHistory = [
...state.history
];
updatedHistory[historyItemIndex] = {
...updatedHistory[historyItemIndex],
params: focusedRoute.params
};
}
return {
...actionResult,
state: {
...nextState,
history: updatedHistory
}
};
}
}
return actionResult;
}
case 'GO_BACK':
{
if (backBehavior === 'none') {
return _types.RouterActionHandling.Unhandled;
}
const focusedRoute = state.routes[state.index];
if (!focusedRoute) {
return _types.RouterActionHandling.Unhandled;
}
let backTargetName;
if (backBehavior === 'firstRoute') {
backTargetName = state.routeNames[0];
} else if (backBehavior === 'initialRoute') {
backTargetName = initialRouteName !== undefined && state.routeNames.includes(initialRouteName) ? initialRouteName : state.routeNames[0];
} else if (backBehavior === 'order') {
const declaredIndex = state.routeNames.indexOf(focusedRoute.name);
backTargetName = declaredIndex > 0 ? state.routeNames[declaredIndex - 1] : undefined;
}
if (backTargetName !== undefined && backTargetName !== focusedRoute.name) {
const { routes, index } = addRouteIfMissing(state.routes, backTargetName, ()=>({
name: backTargetName,
key: nextKey(backTargetName)
}));
if (routes !== state.routes) {
const result = changeIndex({
...state,
routes
}, index, backBehavior, initialRouteName);
return {
state: result,
affectedRouteKey: result.routes[result.index]?.key
};
}
}
if (state.history.length === 1) {
return _types.RouterActionHandling.Unhandled;
}
const previousHistoryItem = state.history[state.history.length - 2];
const previousKey = previousHistoryItem?.key;
const index = state.routes.findLastIndex((route)=>route.key === previousKey);
if (index === -1) {
return _types.RouterActionHandling.Unhandled;
}
let routes = state.routes;
if (backBehavior === 'fullHistory' && routes[index].params !== previousHistoryItem.params) {
routes = [
...state.routes
];
routes[index] = {
...routes[index],
params: previousHistoryItem.params
};
}
return {
state: {
...state,
routes,
history: state.history.slice(0, -1),
index
},
affectedRouteKey: routes[index].key
};
}
case 'PRELOAD':
{
if (!state.routeNames.includes(action.payload.name)) {
return _types.RouterActionHandling.Unhandled;
}
const routeIndex = state.routes.findIndex((route)=>route.name === action.payload.name);
let affectedRouteKey;
let replacedKey;
let routes;
if (routeIndex === -1) {
const route = (0, _attachRouteState.attachRouteState)({
...(0, _createRouteFromAction.createRouteFromAction)({
action,
key: nextKey(action.payload.name)
}),
isPreloaded: true
}, action);
routes = [
...state.routes,
route
];
affectedRouteKey = route.key;
} else {
const route = state.routes[routeIndex];
const getId = routeGetIdList[route.name];
const currentId = getId?.({
params: route.params
});
const nextId = getId?.({
params: action.payload.params
});
const key = currentId === nextId ? route.key : nextKey(route.name);
const params = action.payload.params;
const newRoute = (0, _attachRouteState.attachRouteState)(params !== route.params ? {
...withoutStaleState(route, key),
key,
params,
...key !== route.key && {
isPreloaded: true
}
} : route, action);
replacedKey = key === route.key ? undefined : route.key;
routes = state.routes.map((route, index)=>index === routeIndex ? newRoute : route);
affectedRouteKey = newRoute.key;
}
let history = state.history;
if (backBehavior === 'history' || backBehavior === 'fullHistory') {
if (replacedKey !== undefined) {
// Re-key in place, so the focused route stays the last history entry for `goBack`.
// Only the newest entry takes the new params - `fullHistory` keeps duplicate entries
// and each older one still holds the params of its own visit.
const newRoute = routes[routeIndex];
const newestIndex = history.findLastIndex((record)=>record.type === TYPE_ROUTE && record.key === replacedKey);
history = history.map((record, index)=>record.type === TYPE_ROUTE && record.key === replacedKey ? {
...record,
key: newRoute.key,
params: backBehavior === 'fullHistory' && index === newestIndex ? newRoute.params : record.params
} : record);
}
} else {
const orderedRoutes = getHistoryRoutes(routes, state.routeNames);
const focusedKey = routes[state.index]?.key;
const focusedIndex = orderedRoutes.findIndex((route)=>route.key === focusedKey);
const routeHistory = focusedIndex === -1 ? [] : getRouteHistory(orderedRoutes, focusedIndex, backBehavior, initialRouteName);
// TODO: Refactor history handling together with web state synchronization.
history = [
...routeHistory,
...history.filter((item)=>item.type !== 'route')
];
}
return {
state: {
...state,
routes,
history
},
affectedRouteKey
};
}
default:
{
const result = baseRouter.getStateForAction(state, action, options);
if (!(0, _utils.isActionHandled)(result)) {
return result;
}
return {
...result,
state: ensureStateHistory(result.state, backBehavior, initialRouteName)
};
}
}
},
actionCreators: TabActions
};
return router;
}
const TabRouter = (0, _extendRouter.extendRouter)(_BaseRouter.BaseRouter, tabRouterExtension, {
type: 'tab'
});
function removeReplacedRouteFromHistory(previousState, nextState) {
const replacedRouteKey = previousState.routes[previousState.index]?.key;
const focusedRouteKey = nextState.routes[nextState.index]?.key;
if (!replacedRouteKey || replacedRouteKey === focusedRouteKey) {
return nextState;
}
const replacedIndex = nextState.history.findLastIndex((item)=>item.key === replacedRouteKey);
if (replacedIndex === -1) {
return nextState;
}
return {
...nextState,
history: nextState.history.filter((_, index)=>index !== replacedIndex)
};
}
//# sourceMappingURL=TabRouter.js.map