expo-router
Version:
Expo Router is a file-based router for React Native and web applications.
426 lines (424 loc) • 17.6 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
Object.defineProperty(exports, "getStateFromPath", {
enumerable: true,
get: function() {
return getStateFromPath;
}
});
const _constants = require("../constants");
const _escapeStringRegexp = require("../utils/escapeStringRegexp");
const _findFocusedRoute = require("./findFocusedRoute");
const _getStateFromPathforks = /*#__PURE__*/ _interop_require_wildcard(require("./getStateFromPath-forks"));
const _validatePathConfig = require("./validatePathConfig");
function _getRequireWildcardCache(nodeInterop) {
if (typeof WeakMap !== "function") return null;
var cacheBabelInterop = new WeakMap();
var cacheNodeInterop = new WeakMap();
return (_getRequireWildcardCache = function(nodeInterop) {
return nodeInterop ? cacheNodeInterop : cacheBabelInterop;
})(nodeInterop);
}
function _interop_require_wildcard(obj, nodeInterop) {
if (!nodeInterop && obj && obj.__esModule) {
return obj;
}
if (obj === null || typeof obj !== "object" && typeof obj !== "function") {
return {
default: obj
};
}
var cache = _getRequireWildcardCache(nodeInterop);
if (cache && cache.has(obj)) {
return cache.get(obj);
}
var newObj = {
__proto__: null
};
var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor;
for(var key in obj){
if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) {
var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null;
if (desc && (desc.get || desc.set)) {
Object.defineProperty(newObj, key, desc);
} else {
newObj[key] = obj[key];
}
}
}
newObj.default = obj;
if (cache) {
cache.set(obj, newObj);
}
return newObj;
}
function getStateFromPath(path, options, segments = []) {
const { initialRoutes, configs, configWithRegexes } = getConfigResources(options, segments);
const screens = options?.screens;
const expoPath = _getStateFromPathforks.getUrlWithReactNavigationConcessions(path);
let remaining = _getStateFromPathforks.cleanPath(expoPath.nonstandardPathname);
const prefix = options?.path?.replace(/^\//, ''); // Remove extra leading slash
if (prefix) {
// Make sure there is a trailing slash
const normalizedPrefix = prefix.endsWith('/') ? prefix : `${prefix}/`;
// If the path doesn't start with the prefix, it's not a match
if (!remaining.startsWith(normalizedPrefix)) {
return undefined;
}
// Remove the prefix from the path
remaining = remaining.replace(normalizedPrefix, '');
}
if (screens === undefined) {
// When no config is specified, use the path segments as route names
const routes = remaining.split('/').filter(Boolean).map((segment)=>{
const name = decodeURIComponent(segment);
return {
name
};
});
if (routes.length) {
return createNestedStateObject(expoPath, routes, initialRoutes, [], expoPath.hash);
}
return undefined;
}
if (remaining === '/') {
// We need to add special handling of empty path so navigation to empty path also works
// When handling empty path, we should only look at the root level config
const match = _getStateFromPathforks.matchForEmptyPath(configWithRegexes);
if (match) {
return createNestedStateObject(expoPath, match.routeNames.map((name)=>({
name
})), initialRoutes, configs, expoPath.hash);
}
return undefined;
}
let result;
let current;
// We match the whole path against the regex instead of segments
// This makes sure matches such as wildcard will catch any unmatched routes, even if nested
const { routes, remainingPath } = matchAgainstConfigs(remaining, configWithRegexes);
if (routes !== undefined) {
// This will always be empty if full path matched
current = createNestedStateObject(expoPath, routes, initialRoutes, configs, expoPath.hash);
remaining = remainingPath;
result = current;
}
if (current == null || result == null) {
return undefined;
}
return result;
}
/**
* Reference to the most recently computed config resources.
*/ let cachedConfigResources = [
undefined,
prepareConfigResources()
];
function getConfigResources(options, previousSegments) {
// Recompute every time because config resources depend on the current state.
cachedConfigResources = [
options,
prepareConfigResources(options, previousSegments)
];
return cachedConfigResources[1];
}
function prepareConfigResources(options, previousSegments) {
if (options) {
(0, _validatePathConfig.validatePathConfig)(options);
}
const initialRoutes = getInitialRoutes(options);
const configs = getNormalizedConfigs(initialRoutes, options?.screens, previousSegments);
checkForDuplicatedConfigs(configs);
const configWithRegexes = getConfigsWithRegexes(configs);
return {
initialRoutes,
configs,
configWithRegexes
};
}
function getInitialRoutes(options) {
const initialRoutes = [];
if (options?.initialRouteName) {
initialRoutes.push({
initialRouteName: options.initialRouteName,
parentScreens: []
});
}
return initialRoutes;
}
function getNormalizedConfigs(initialRoutes, screens = {}, previousSegments) {
// Create a normalized configs array which will be easier to use
return [].concat(...Object.keys(screens).map((key)=>createNormalizedConfigs(key, screens, [], initialRoutes, []))).map(_getStateFromPathforks.appendIsInitial(initialRoutes)).sort(_getStateFromPathforks.getRouteConfigSorter(previousSegments));
}
function checkForDuplicatedConfigs(configs) {
// Check for duplicate patterns in the config
configs.reduce((acc, config)=>{
if (acc[config.pattern]) {
const a = acc[config.pattern].routeNames;
const b = config.routeNames;
// It's not a problem if the path string omitted from a inner most screen
// For example, it's ok if a path resolves to `A > B > C` or `A > B`
const intersects = a.length > b.length ? b.every((it, i)=>a[i] === it) : a.every((it, i)=>b[i] === it);
if (!intersects) {
throw new Error(`Found conflicting screens with the same pattern. The pattern '${config.pattern}' resolves to both '${a.join(' > ')}' and '${b.join(' > ')}'. Patterns must be unique and cannot resolve to more than one screen.`);
}
}
return Object.assign(acc, {
[config.pattern]: config
});
}, {});
}
function getConfigsWithRegexes(configs) {
return configs.map((c)=>({
...c,
// Add `$` to the regex to make sure it matches till end of the path and not just beginning
regex: _getStateFromPathforks.configRegExp(c)
}));
}
const joinPaths = (...paths)=>[].concat(...paths.map((p)=>p.split('/'))).filter(Boolean).join('/');
const matchAgainstConfigs = (remaining, configs)=>{
let routes;
let remainingPath = remaining;
const allParams = Object.create(null);
// Go through all configs, and see if the next path segment matches our regex
for (const config of configs){
if (!config.regex) {
continue;
}
const match = remainingPath.match(config.regex);
// If our regex matches, we need to extract params from the path
if (match) {
const matchResult = config.pattern?.split('/').reduce((acc, p, index)=>{
if (!_getStateFromPathforks.isDynamicPart(p)) {
return acc;
}
acc.pos += 1;
const decodedParamSegment = _getStateFromPathforks.safelyDecodeURIComponent(// The param segments appear every second item starting from 2 in the regex match result
match[(acc.pos + 1) * 2].replace(/\/$/, ''));
Object.assign(acc.matchedParams, {
[p]: Object.assign(acc.matchedParams[p] || {}, {
[index]: decodedParamSegment
})
});
return acc;
}, {
pos: -1,
matchedParams: {}
});
const matchedParams = matchResult.matchedParams || {};
routes = config.routeNames.map((name)=>{
const routeConfig = configs.find((c)=>{
// Check matching name AND pattern in case same screen is used at different levels in config
return c.screen === name && config.pattern.startsWith(c.pattern);
});
// Normalize pattern to remove any leading, trailing slashes, duplicate slashes etc.
const normalizedPath = routeConfig?.path.split('/').filter(Boolean).join('/');
// Get the number of segments in the initial pattern
const numInitialSegments = routeConfig?.pattern.replace(new RegExp(`${(0, _escapeStringRegexp.escapeStringRegexp)(normalizedPath)}$`), '')?.split('/').length;
const params = normalizedPath?.split('/').reduce((acc, p, index)=>{
if (!_getStateFromPathforks.isDynamicPart(p)) {
return acc;
}
// Get the real index of the path parameter in the matched path
// by offsetting by the number of segments in the initial pattern
const offset = numInitialSegments ? numInitialSegments - 1 : 0;
// TODO(@kitten): Assess which is intended, non-optional or getParamValue accepting undefined
const value = _getStateFromPathforks.getParamValue(p, matchedParams[p]?.[index + offset]);
if (value) {
const key = _getStateFromPathforks.replacePart(p);
acc[key] = routeConfig?.parse?.[key] ? routeConfig.parse[key](value) : value;
}
return acc;
}, {});
if (params && Object.keys(params).length) {
Object.assign(allParams, params);
return {
name,
params
};
}
return {
name
};
});
remainingPath = remainingPath.replace(match[1], '');
break;
}
}
_getStateFromPathforks.populateParams(routes, allParams);
return {
routes,
remainingPath
};
};
const createNormalizedConfigs = (screen, routeConfig, routeNames = [], initials, parentScreens, parentPattern)=>{
const configs = [];
routeNames.push(screen);
parentScreens.push(screen);
// @ts-expect-error: TODO(@kitten): This is entirely untyped. The index access just flags this, but we're not typing the config properly here
const config = routeConfig[screen];
if (typeof config === 'string') {
// If a string is specified as the value of the key(e.g. Foo: '/path'), use it as the pattern
const pattern = parentPattern ? joinPaths(parentPattern, config) : config;
configs.push(createConfigItem(screen, routeNames, pattern, config));
} else if (typeof config === 'object') {
let pattern;
// if an object is specified as the value (e.g. Foo: { ... }),
// it can have `path` property and
// it could have `screens` prop which has nested configs
if (typeof config.path === 'string') {
if (config.exact && config.path === undefined) {
throw new Error("A 'path' needs to be specified when specifying 'exact: true'. If you don't want this screen in the URL, specify it as empty string, e.g. `path: ''`.");
}
pattern = config.exact !== true ? joinPaths(parentPattern || '', config.path || '') : config.path || '';
if (screen !== _constants.INTERNAL_SLOT_NAME) {
configs.push(createConfigItem(screen, routeNames, pattern, config.path, config.parse, config));
}
}
if (config.screens) {
// property `initialRouteName` without `screens` has no purpose
if (config.initialRouteName) {
initials.push({
initialRouteName: config.initialRouteName,
parentScreens
});
}
Object.keys(config.screens).forEach((nestedConfig)=>{
const result = createNormalizedConfigs(nestedConfig, config.screens, routeNames, initials, [
...parentScreens
], pattern ?? parentPattern);
configs.push(...result);
});
}
}
routeNames.pop();
return configs;
};
const createConfigItem = (screen, routeNames, pattern, path, parse = undefined, config = {})=>{
// Normalize pattern to remove any leading, trailing slashes, duplicate slashes etc.
pattern = pattern.split('/').filter(Boolean).join('/');
const regex = pattern ? _getStateFromPathforks.routePatternToRegex(pattern) : undefined;
return {
screen,
regex,
pattern,
path,
// The routeNames array is mutated, so copy it to keep the current state
routeNames: [
...routeNames
],
parse,
..._getStateFromPathforks.createConfig(screen, pattern, routeNames, config)
};
};
const findParseConfigForRoute = (routeName, flatConfig)=>{
for (const config of flatConfig){
if (routeName === config.routeNames[config.routeNames.length - 1]) {
return config.parse;
}
}
return undefined;
};
// Try to find an initial route connected with the one passed
const findInitialRoute = (routeName, parentScreens, initialRoutes)=>{
for (const config of initialRoutes){
if (parentScreens.length === config.parentScreens.length) {
let sameParents = true;
for(let i = 0; i < parentScreens.length; i++){
if (parentScreens[i].localeCompare(config.parentScreens[i]) !== 0) {
sameParents = false;
break;
}
}
if (sameParents) {
return routeName !== config.initialRouteName ? config.initialRouteName : undefined;
}
}
}
return undefined;
};
// returns state object with values depending on whether
// it is the end of state and if there is initialRoute for this level
const createStateObject = (initialRoute, route, isEmpty)=>{
if (isEmpty) {
if (initialRoute) {
return {
index: 1,
routes: [
{
name: initialRoute,
params: route.params
},
route
]
};
} else {
return {
routes: [
route
]
};
}
} else {
if (initialRoute) {
return {
index: 1,
routes: [
{
name: initialRoute,
params: route.params
},
{
...route,
state: {
routes: []
}
}
]
};
} else {
return {
routes: [
{
...route,
state: {
routes: []
}
}
]
};
}
}
};
const createNestedStateObject = ({ path, ...expoURL }, routes, initialRoutes, flatConfig, hash)=>{
let route = routes.shift();
const parentScreens = [];
let initialRoute = findInitialRoute(route.name, parentScreens, initialRoutes);
parentScreens.push(route.name);
const state = createStateObject(initialRoute, route, routes.length === 0);
if (routes.length > 0) {
let nestedState = state;
while(route = routes.shift()){
initialRoute = findInitialRoute(route.name, parentScreens, initialRoutes);
const nestedStateIndex = nestedState.index ?? nestedState.routes.length - 1;
nestedState.routes[nestedStateIndex].state = createStateObject(initialRoute, route, routes.length === 0);
if (routes.length > 0) {
nestedState = nestedState.routes[nestedStateIndex].state;
}
parentScreens.push(route.name);
}
}
route = (0, _findFocusedRoute.findFocusedRoute)(state);
route.path = expoURL.pathWithoutGroups;
const params = _getStateFromPathforks.parseQueryParams(path, route, flatConfig ? findParseConfigForRoute(route.name, flatConfig) : undefined, hash);
if (params) {
route.params = {
...route.params,
...params
};
}
return state;
};
//# sourceMappingURL=getStateFromPath.js.map