expo-router
Version:
Expo Router is a file-based router for React Native and web applications.
526 lines • 21.4 kB
JavaScript
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getStateFromPath = void 0;
const native_1 = require("@react-navigation/native");
const escape_string_regexp_1 = __importDefault(require("escape-string-regexp"));
const findFocusedRoute_1 = require("./findFocusedRoute");
const expo = __importStar(require("./getStateFromPath-forks"));
/**
* Utility to parse a path string to initial state object accepted by the container.
* This is useful for deep linking when we need to handle the incoming URL.
*
* @example
* ```js
* getStateFromPath(
* '/chat/jane/42',
* {
* screens: {
* Chat: {
* path: 'chat/:author/:id',
* parse: { id: Number }
* }
* }
* }
* )
* ```
* @param path Path string to parse and convert, e.g. /foo/bar?count=42.
* @param options Extra options to fine-tune how to parse the path.
*/
function getStateFromPath(
// END FORK
path, options) {
const { initialRoutes, configs, configWithRegexes } = getConfigResources(options, this?.routeInfo?.segments);
const screens = options?.screens;
// START FORK
const expoPath = expo.getUrlWithReactNavigationConcessions(path);
// END FORK
// START FORK
let remaining = expoPath.nonstandardPathname
// let remaining = path
// END FORK
.replace(/\/+/g, '/') // Replace multiple slash (//) with single ones
.replace(/^\//, '') // Remove extra leading slash
.replace(/\?.*$/, ''); // Remove query params which we will handle later
// Make sure there is a trailing slash
remaining = remaining.endsWith('/') ? remaining : `${remaining}/`;
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.url.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
// START FORK
const match = expo.matchForEmptyPath(configWithRegexes);
// const match = configs.find(
// (config) =>
// config.path === '' &&
// config.routeNames.every(
// // Make sure that none of the parent configs have a non-empty path defined
// (name) => !configs.find((c) => c.screen === name)?.path
// )
// );
// END FORK
if (match) {
return createNestedStateObject(expoPath, match.routeNames.map((name) => ({ name })), initialRoutes, configs, expoPath.url.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.url.hash);
remaining = remainingPath;
result = current;
}
if (current == null || result == null) {
return undefined;
}
return result;
}
exports.getStateFromPath = getStateFromPath;
/**
* Reference to the last used config resources. This is used to avoid recomputing the config resources when the options are the same.
*/
let cachedConfigResources = [
undefined,
prepareConfigResources(),
];
function getConfigResources(options,
// START FORK
previousSegments
// END FORK
) {
// START FORK - We need to disable this caching as our configs can change based upon the current state
// if (cachedConfigResources[0] !== options) {
// console.log(previousSegments);
cachedConfigResources = [options, prepareConfigResources(options, previousSegments)];
// }
// END FORK FORK
return cachedConfigResources[1];
}
function prepareConfigResources(options, previousSegments) {
if (options) {
(0, native_1.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 = {},
// START FORK
previousSegments
// END FORK
) {
// Create a normalized configs array which will be easier to use
return []
.concat(...Object.keys(screens).map((key) => createNormalizedConfigs(key, screens, [], initialRoutes, [])))
.map(expo.appendIsInitial(initialRoutes))
.sort(expo.getRouteConfigSorter(previousSegments));
// .sort((a, b) => {
// // Sort config so that:
// // - the most exhaustive ones are always at the beginning
// // - patterns with wildcard are always at the end
// // If 2 patterns are same, move the one with less route names up
// // This is an error state, so it's only useful for consistent error messages
// if (a.pattern === b.pattern) {
// return b.routeNames.join('>').localeCompare(a.routeNames.join('>'));
// }
// // If one of the patterns starts with the other, it's more exhaustive
// // So move it up
// if (a.pattern.startsWith(b.pattern)) {
// return -1;
// }
// if (b.pattern.startsWith(a.pattern)) {
// return 1;
// }
// const aParts = a.pattern.split('/');
// const bParts = b.pattern.split('/');
// for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
// // if b is longer, b get higher priority
// if (aParts[i] == null) {
// return 1;
// }
// // if a is longer, a get higher priority
// if (bParts[i] == null) {
// return -1;
// }
// const aWildCard = aParts[i] === '*' || aParts[i].startsWith(':');
// const bWildCard = bParts[i] === '*' || bParts[i].startsWith(':');
// // if both are wildcard we compare next component
// if (aWildCard && bWildCard) {
// continue;
// }
// // if only a is wild card, b get higher priority
// if (aWildCard) {
// return 1;
// }
// // if only b is wild card, a get higher priority
// if (bWildCard) {
// return -1;
// }
// }
// return bParts.length - aParts.length;
// });
}
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
// START FORK
// regex: c.regex ? new RegExp(c.regex.source + '$') : undefined,
regex: expo.configRegExp(c),
// END FORK
}));
}
const joinPaths = (...paths) => []
.concat(...paths.map((p) => p.split('/')))
.filter(Boolean)
.join('/');
const matchAgainstConfigs = (remaining, configs) => {
let routes;
let remainingPath = remaining;
// START FORK
const allParams = Object.create(null);
// END FORK
// 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 (!expo.isDynamicPart(p)) {
return acc;
}
acc.pos += 1;
// START FORK
const decodedParamSegment = expo.safelyDecodeURIComponent(
// const decodedParamSegment = decodeURIComponent(
// The param segments appear every second item starting from 2 in the regex match result
match[(acc.pos + 1) * 2]
// Remove trailing slash
.replace(/\/$/, ''));
// END FORK
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
// Extract the prefix from the pattern by removing the ending path pattern (e.g pattern=`a/b/c/d` and normalizedPath=`c/d` becomes `a/b`)
.replace(new RegExp(`${(0, escape_string_regexp_1.default)(normalizedPath)}$`), '')
?.split('/').length;
const params = normalizedPath
?.split('/')
.reduce((acc, p, index) => {
if (!expo.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;
// START FORK
// const value = matchedParams[p]?.[index + offset];
const value = expo.getParamValue(p, matchedParams[p]?.[index + offset]);
// END FORK
if (value) {
// START FORK
// const key = p.replace(/^:/, '').replace(/\?$/, '');
const key = expo.replacePart(p);
// END FORK
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;
}
}
// START FORK
expo.populateParams(routes, allParams);
// END FORK
return { routes, remainingPath };
};
const createNormalizedConfigs = (screen, routeConfig, routeNames = [], initials, parentScreens, parentPattern) => {
const configs = [];
routeNames.push(screen);
parentScreens.push(screen);
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 || '';
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
? new RegExp(`^(${pattern
.split('/')
.map((it) => {
if (it.startsWith(':')) {
return `(([^/]+\\/)${it.endsWith('?') ? '?' : ''})`;
}
return `${it === '*' ? '.*' : (0, escape_string_regexp_1.default)(it)}\\/`;
})
.join('')})`)
: undefined;
return {
screen,
regex,
pattern,
path,
// The routeNames array is mutated, so copy it to keep the current state
routeNames: [...routeNames],
parse,
// START FORK
...expo.createConfig(screen, pattern, routeNames, config),
// END FORK
};
};
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_1.findFocusedRoute)(state);
// START FORK
route.path = expoURL.pathWithoutGroups;
// route.path = path;
// END FORK
// START FORK
// const params = parseQueryParams(
const params = expo.parseQueryParams(path, route, flatConfig ? findParseConfigForRoute(route.name, flatConfig) : undefined, hash);
// END FORK
// START FORK
// expo.handleUrlParams(route, params, hash);
if (params) {
route.params = { ...route.params, ...params };
}
// END FORK
return state;
};
// START FORK
// const parseQueryParams = (path: string, parseConfig?: Record<string, (value: string) => any>) => {
// const query = path.split('?')[1];
// const params = queryString.parse(query);
// if (parseConfig) {
// Object.keys(params).forEach((name) => {
// if (Object.hasOwnProperty.call(parseConfig, name) && typeof params[name] === 'string') {
// params[name] = parseConfig[name](params[name] as string);
// }
// });
// }
// return Object.keys(params).length ? params : undefined;
// };
// END FORK
//# sourceMappingURL=getStateFromPath.js.map
;