1"use strict"; 2var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { 3 if (k2 === undefined) k2 = k; 4 var desc = Object.getOwnPropertyDescriptor(m, k); 5 if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { 6 desc = { enumerable: true, get: function() { return m[k]; } }; 7 } 8 Object.defineProperty(o, k2, desc); 9}) : (function(o, m, k, k2) { 10 if (k2 === undefined) k2 = k; 11 o[k2] = m[k]; 12})); 13var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { 14 Object.defineProperty(o, "default", { enumerable: true, value: v }); 15}) : function(o, v) { 16 o["default"] = v; 17}); 18var __importStar = (this && this.__importStar) || function (mod) { 19 if (mod && mod.__esModule) return mod; 20 var result = {}; 21 if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); 22 __setModuleDefault(result, mod); 23 return result; 24}; 25var __importDefault = (this && this.__importDefault) || function (mod) { 26 return (mod && mod.__esModule) ? mod : { "default": mod }; 27}; 28Object.defineProperty(exports, "__esModule", { value: true }); 29exports.stripBasePath = exports.getMatchableRouteConfigs = exports.getUrlWithReactNavigationConcessions = void 0; 30const escape_string_regexp_1 = __importDefault(require("escape-string-regexp")); 31const expo_constants_1 = __importDefault(require("expo-constants")); 32const queryString = __importStar(require("query-string")); 33const url_parse_1 = __importDefault(require("url-parse")); 34const findFocusedRoute_1 = require("./findFocusedRoute"); 35const validatePathConfig_1 = __importDefault(require("./validatePathConfig")); 36const matchers_1 = require("../matchers"); 37function getUrlWithReactNavigationConcessions(path, basePath = expo_constants_1.default.expoConfig?.experiments?.basePath) { 38 const parsed = new url_parse_1.default(path, 'https://acme.com'); 39 const pathname = parsed.pathname; 40 // Make sure there is a trailing slash 41 return { 42 // The slashes are at the end, not the beginning 43 nonstandardPathname: stripBasePath(pathname, basePath).replace(/^\/+/g, '').replace(/\/+$/g, '') + '/', 44 // React Navigation doesn't support hashes, so here 45 inputPathnameWithoutHash: stripBasePath(path, basePath).replace(/#.*$/, ''), 46 }; 47} 48exports.getUrlWithReactNavigationConcessions = getUrlWithReactNavigationConcessions; 49/** 50 * Utility to parse a path string to initial state object accepted by the container. 51 * This is useful for deep linking when we need to handle the incoming URL. 52 * 53 * @example 54 * ```js 55 * getStateFromPath( 56 * '/chat/jane/42', 57 * { 58 * screens: { 59 * Chat: { 60 * path: 'chat/:author/:id', 61 * parse: { id: Number } 62 * } 63 * } 64 * } 65 * ) 66 * ``` 67 * @param path Path string to parse and convert, e.g. /foo/bar?count=42. 68 * @param options Extra options to fine-tune how to parse the path. 69 */ 70function getStateFromPath(path, options) { 71 const { initialRoutes, configs } = getMatchableRouteConfigs(options); 72 return getStateFromPathWithConfigs(path, configs, initialRoutes); 73} 74exports.default = getStateFromPath; 75function getMatchableRouteConfigs(options) { 76 if (options) { 77 (0, validatePathConfig_1.default)(options); 78 } 79 const screens = options?.screens; 80 // Expo Router disallows usage without a linking config. 81 if (!screens) { 82 throw Error("You must pass a 'screens' object to 'getStateFromPath' to generate a path."); 83 } 84 // This will be mutated... 85 const initialRoutes = []; 86 if (options?.initialRouteName) { 87 initialRoutes.push({ 88 initialRouteName: options.initialRouteName, 89 parentScreens: [], 90 }); 91 } 92 // Create a normalized configs array which will be easier to use. 93 const converted = Object.keys(screens) 94 .map((key) => createNormalizedConfigs(key, screens, [], initialRoutes)) 95 .flat(); 96 const resolvedInitialPatterns = initialRoutes.map((route) => joinPaths(...route.parentScreens, route.initialRouteName)); 97 const convertedWithInitial = converted.map((config) => ({ 98 ...config, 99 // TODO(EvanBacon): Probably a safer way to do this 100 // Mark initial routes to give them potential priority over other routes that match. 101 isInitial: resolvedInitialPatterns.includes(config.routeNames.join('/')), 102 })); 103 // Sort in order of resolution. This is extremely important for the algorithm to work. 104 const configs = convertedWithInitial.sort(sortConfigs); 105 // Assert any duplicates before we start parsing. 106 assertConfigDuplicates(configs); 107 return { configs, initialRoutes }; 108} 109exports.getMatchableRouteConfigs = getMatchableRouteConfigs; 110function assertConfigDuplicates(configs) { 111 // Check for duplicate patterns in the config 112 configs.reduce((acc, config) => { 113 // NOTE(EvanBacon): Uses the regex pattern as key to detect duplicate slugs. 114 const indexedKey = config.regex?.toString() ?? config.pattern; 115 const alpha = acc[indexedKey]; 116 // NOTE(EvanBacon): Skips checking nodes that have children. 117 if (alpha && !alpha.hasChildren && !config.hasChildren) { 118 const a = alpha.routeNames; 119 const b = config.routeNames; 120 // It's not a problem if the path string omitted from a inner most screen 121 // For example, it's ok if a path resolves to `A > B > C` or `A > B` 122 const intersects = a.length > b.length ? b.every((it, i) => a[i] === it) : a.every((it, i) => b[i] === it); 123 if (!intersects) { 124 // NOTE(EvanBacon): Adds more context to the error message since we know about the 125 // file-based routing. 126 const last = config.pattern.split('/').pop(); 127 const routeType = last?.startsWith(':') 128 ? 'dynamic route' 129 : last?.startsWith('*') 130 ? 'dynamic-rest route' 131 : 'route'; 132 throw new Error(`The ${routeType} pattern '${config.pattern || '/'}' resolves to both '${alpha.userReadableName}' and '${config.userReadableName}'. Patterns must be unique and cannot resolve to more than one route.`); 133 } 134 } 135 return Object.assign(acc, { 136 [indexedKey]: config, 137 }); 138 }, {}); 139} 140function sortConfigs(a, b) { 141 // Sort config so that: 142 // - the most exhaustive ones are always at the beginning 143 // - patterns with wildcard are always at the end 144 // If 2 patterns are same, move the one with less route names up 145 // This is an error state, so it's only useful for consistent error messages 146 if (a.pattern === b.pattern) { 147 return b.routeNames.join('>').localeCompare(a.routeNames.join('>')); 148 } 149 // If one of the patterns starts with the other, it's more exhaustive 150 // So move it up 151 if (a.pattern.startsWith(b.pattern) && 152 // NOTE(EvanBacon): This is a hack to make sure that `*` is always at the end 153 b.screen !== 'index') { 154 return -1; 155 } 156 if (b.pattern.startsWith(a.pattern) && a.screen !== 'index') { 157 return 1; 158 } 159 // NOTE(EvanBacon): Here we append `index` if the screen was `index` so the length is the same 160 // as a slug or wildcard when nested more than one level deep. 161 // This is so we can compare the length of the pattern, e.g. `foo/*` > `foo` vs `*` < ``. 162 const aParts = a.pattern 163 .split('/') 164 // Strip out group names to ensure they don't affect the priority. 165 .filter((part) => (0, matchers_1.matchGroupName)(part) == null); 166 if (a.screen === 'index') { 167 aParts.push('index'); 168 } 169 const bParts = b.pattern.split('/').filter((part) => (0, matchers_1.matchGroupName)(part) == null); 170 if (b.screen === 'index') { 171 bParts.push('index'); 172 } 173 for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) { 174 // if b is longer, b get higher priority 175 if (aParts[i] == null) { 176 return 1; 177 } 178 // if a is longer, a get higher priority 179 if (bParts[i] == null) { 180 return -1; 181 } 182 const aWildCard = aParts[i].startsWith('*'); 183 const bWildCard = bParts[i].startsWith('*'); 184 // if both are wildcard we compare next component 185 if (aWildCard && bWildCard) { 186 continue; 187 } 188 // if only a is wild card, b get higher priority 189 if (aWildCard) { 190 return 1; 191 } 192 // if only b is wild card, a get higher priority 193 if (bWildCard) { 194 return -1; 195 } 196 const aSlug = aParts[i].startsWith(':'); 197 const bSlug = bParts[i].startsWith(':'); 198 // if both are wildcard we compare next component 199 if (aSlug && bSlug) { 200 continue; 201 } 202 // if only a is wild card, b get higher priority 203 if (aSlug) { 204 return 1; 205 } 206 // if only b is wild card, a get higher priority 207 if (bSlug) { 208 return -1; 209 } 210 } 211 // Sort initial routes with a higher priority than routes which will push more screens 212 // this ensures shared routes go to the shortest path. 213 if (a.isInitial && !b.isInitial) { 214 return -1; 215 } 216 if (!a.isInitial && b.isInitial) { 217 return 1; 218 } 219 return bParts.length - aParts.length; 220} 221function getStateFromEmptyPathWithConfigs(path, configs, initialRoutes) { 222 // We need to add special handling of empty path so navigation to empty path also works 223 // When handling empty path, we should only look at the root level config 224 // NOTE(EvanBacon): We only care about matching leaf nodes. 225 const leafNodes = configs 226 .filter((config) => !config.hasChildren) 227 .map((value) => { 228 return { 229 ...value, 230 // Collapse all levels of group segments before testing. 231 // This enables `app/(one)/(two)/index.js` to be matched. 232 path: (0, matchers_1.stripGroupSegmentsFromPath)(value.path), 233 }; 234 }); 235 const match = leafNodes.find((config) => 236 // NOTE(EvanBacon): Test leaf node index routes that either don't have a regex or match an empty string. 237 config.path === '' && (!config.regex || config.regex.test(''))) ?? 238 leafNodes.find((config) => 239 // NOTE(EvanBacon): Test leaf node dynamic routes that match an empty string. 240 config.path.startsWith(':') && config.regex.test('')) ?? 241 // NOTE(EvanBacon): Test leaf node deep dynamic routes that match a slash. 242 // This should be done last to enable dynamic routes having a higher priority. 243 leafNodes.find((config) => config.path.startsWith('*') && config.regex.test('/')); 244 if (!match) { 245 return undefined; 246 } 247 const routes = match.routeNames.map((name) => { 248 if (!match._route) { 249 return { name }; 250 } 251 return { 252 name, 253 _route: match._route, 254 }; 255 }); 256 return createNestedStateObject(path, routes, configs, initialRoutes); 257} 258function getStateFromPathWithConfigs(path, configs, initialRoutes) { 259 const formattedPaths = getUrlWithReactNavigationConcessions(path); 260 if (formattedPaths.nonstandardPathname === '/') { 261 return getStateFromEmptyPathWithConfigs(formattedPaths.inputPathnameWithoutHash, configs, initialRoutes); 262 } 263 // We match the whole path against the regex instead of segments 264 // This makes sure matches such as wildcard will catch any unmatched routes, even if nested 265 const routes = matchAgainstConfigs(formattedPaths.nonstandardPathname, configs); 266 if (routes == null) { 267 return undefined; 268 } 269 // This will always be empty if full path matched 270 return createNestedStateObject(formattedPaths.inputPathnameWithoutHash, routes, configs, initialRoutes); 271} 272const joinPaths = (...paths) => [] 273 .concat(...paths.map((p) => p.split('/'))) 274 .filter(Boolean) 275 .join('/'); 276function matchAgainstConfigs(remaining, configs) { 277 let routes; 278 let remainingPath = remaining; 279 // Go through all configs, and see if the next path segment matches our regex 280 for (const config of configs) { 281 if (!config.regex) { 282 continue; 283 } 284 const match = remainingPath.match(config.regex); 285 // If our regex matches, we need to extract params from the path 286 if (!match) { 287 continue; 288 } 289 // TODO: Add support for wildcard routes 290 const matchedParams = config.pattern 291 ?.split('/') 292 .filter((p) => p.match(/^[:*]/)) 293 .reduce((acc, p, i) => { 294 if (p.match(/^\*/)) { 295 return { 296 ...acc, 297 [p]: match[(i + 1) * 2], //?.replace(/\//, ""), 298 }; 299 } 300 return Object.assign(acc, { 301 // The param segments appear every second item starting from 2 in the regex match result. 302 // This will only work if we ensure groups aren't included in the match. 303 [p]: match[(i + 1) * 2]?.replace(/\//, ''), 304 }); 305 }, {}); 306 const routeFromName = (name) => { 307 const config = configs.find((c) => c.screen === name); 308 if (!config?.path) { 309 return { name }; 310 } 311 const segments = config.path.split('/'); 312 const params = {}; 313 segments 314 .filter((p) => p.match(/^[:*]/)) 315 .forEach((p) => { 316 let value = matchedParams[p]; 317 if (value) { 318 if (p.match(/^\*/)) { 319 // Convert to an array before providing as a route. 320 value = value?.split('/').filter(Boolean); 321 } 322 const key = p.replace(/^[:*]/, '').replace(/\?$/, ''); 323 params[key] = config.parse?.[key] ? config.parse[key](value) : value; 324 } 325 }); 326 if (params && Object.keys(params).length) { 327 return { name, params }; 328 } 329 return { name }; 330 }; 331 routes = config.routeNames.map((name) => { 332 if (!config._route) { 333 return { ...routeFromName(name) }; 334 } 335 return { 336 ...routeFromName(name), 337 _route: config._route, 338 }; 339 }); 340 // TODO(EvanBacon): Maybe we should warn / assert if multiple slugs use the same param name. 341 const combinedParams = routes.reduce((acc, r) => Object.assign(acc, r.params), {}); 342 const hasCombinedParams = Object.keys(combinedParams).length > 0; 343 // Combine all params so a route `[foo]/[bar]/other.js` has access to `{ foo, bar }` 344 routes = routes.map((r) => { 345 if (hasCombinedParams) { 346 r.params = combinedParams; 347 } 348 return r; 349 }); 350 remainingPath = remainingPath.replace(match[1], ''); 351 break; 352 } 353 return routes; 354} 355function equalHeritage(a, b) { 356 if (a.length !== b.length) { 357 return false; 358 } 359 for (let i = 0; i < a.length; i++) { 360 if (a[i].localeCompare(b[i]) !== 0) { 361 return false; 362 } 363 } 364 return true; 365} 366const createNormalizedConfigs = (screen, routeConfig, routeNames = [], initials = [], parentScreens = [], parentPattern) => { 367 const configs = []; 368 routeNames.push(screen); 369 parentScreens.push(screen); 370 const config = routeConfig[screen]; 371 if (typeof config === 'string') { 372 // TODO: This should never happen with the addition of `_route` 373 // If a string is specified as the value of the key(e.g. Foo: '/path'), use it as the pattern 374 const pattern = parentPattern ? joinPaths(parentPattern, config) : config; 375 configs.push(createConfigItem(screen, routeNames, pattern, config, false)); 376 } 377 else if (typeof config === 'object') { 378 let pattern; 379 const { _route } = config; 380 // if an object is specified as the value (e.g. Foo: { ... }), 381 // it can have `path` property and 382 // it could have `screens` prop which has nested configs 383 if (typeof config.path === 'string') { 384 if (config.exact && config.path === undefined) { 385 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: ''`."); 386 } 387 pattern = 388 config.exact !== true 389 ? joinPaths(parentPattern || '', config.path || '') 390 : config.path || ''; 391 configs.push(createConfigItem(screen, routeNames, pattern, config.path, config.screens ? !!Object.keys(config.screens)?.length : false, config.parse, _route)); 392 } 393 if (config.screens) { 394 // property `initialRouteName` without `screens` has no purpose 395 if (config.initialRouteName) { 396 initials.push({ 397 initialRouteName: config.initialRouteName, 398 parentScreens, 399 }); 400 } 401 Object.keys(config.screens).forEach((nestedConfig) => { 402 const result = createNormalizedConfigs(nestedConfig, config.screens, routeNames, initials, [...parentScreens], pattern ?? parentPattern); 403 configs.push(...result); 404 }); 405 } 406 } 407 routeNames.pop(); 408 return configs; 409}; 410function formatRegexPattern(it) { 411 // Allow spaces in file path names. 412 it = it.replace(' ', '%20'); 413 if (it.startsWith(':')) { 414 // TODO: Remove unused match group 415 return `(([^/]+\\/)${it.endsWith('?') ? '?' : ''})`; 416 } 417 else if (it.startsWith('*')) { 418 return `((.*\\/)${it.endsWith('?') ? '?' : ''})`; 419 } 420 // Strip groups from the matcher 421 if ((0, matchers_1.matchGroupName)(it) != null) { 422 // Groups are optional segments 423 // this enables us to match `/bar` and `/(foo)/bar` for the same route 424 // NOTE(EvanBacon): Ignore this match in the regex to avoid capturing the group 425 return `(?:${(0, escape_string_regexp_1.default)(it)}\\/)?`; 426 } 427 return (0, escape_string_regexp_1.default)(it) + `\\/`; 428} 429const createConfigItem = (screen, routeNames, pattern, path, hasChildren, parse, _route) => { 430 // Normalize pattern to remove any leading, trailing slashes, duplicate slashes etc. 431 pattern = pattern.split('/').filter(Boolean).join('/'); 432 const regex = pattern 433 ? new RegExp(`^(${pattern.split('/').map(formatRegexPattern).join('')})$`) 434 : undefined; 435 return { 436 screen, 437 regex, 438 pattern, 439 path, 440 // The routeNames array is mutated, so copy it to keep the current state 441 routeNames: [...routeNames], 442 parse, 443 userReadableName: [...routeNames.slice(0, -1), path || screen].join('/'), 444 hasChildren: !!hasChildren, 445 _route, 446 }; 447}; 448const findParseConfigForRoute = (routeName, routeConfigs) => { 449 for (const config of routeConfigs) { 450 if (routeName === config.routeNames[config.routeNames.length - 1]) { 451 return config.parse; 452 } 453 } 454 return undefined; 455}; 456// Try to find an initial route connected with the one passed 457const findInitialRoute = (routeName, parentScreens, initialRoutes) => { 458 for (const config of initialRoutes) { 459 if (equalHeritage(parentScreens, config.parentScreens)) { 460 // If the parents are the same but the route name doesn't match the initial route 461 // then we return the initial route. 462 return routeName !== config.initialRouteName ? config.initialRouteName : undefined; 463 } 464 } 465 return undefined; 466}; 467// returns state object with values depending on whether 468// it is the end of state and if there is initialRoute for this level 469const createStateObject = (initialRoute, route, isEmpty) => { 470 if (isEmpty) { 471 if (initialRoute) { 472 return { 473 index: 1, 474 routes: [{ name: initialRoute }, route], 475 }; 476 } 477 return { 478 routes: [route], 479 }; 480 } 481 if (initialRoute) { 482 return { 483 index: 1, 484 routes: [{ name: initialRoute }, { ...route, state: { routes: [] } }], 485 }; 486 } 487 return { 488 routes: [{ ...route, state: { routes: [] } }], 489 }; 490}; 491const createNestedStateObject = (path, routes, routeConfigs, initialRoutes) => { 492 let route = routes.shift(); 493 const parentScreens = []; 494 let initialRoute = findInitialRoute(route.name, parentScreens, initialRoutes); 495 parentScreens.push(route.name); 496 const state = createStateObject(initialRoute, route, routes.length === 0); 497 if (routes.length > 0) { 498 let nestedState = state; 499 while ((route = routes.shift())) { 500 initialRoute = findInitialRoute(route.name, parentScreens, initialRoutes); 501 const nestedStateIndex = nestedState.index || nestedState.routes.length - 1; 502 nestedState.routes[nestedStateIndex].state = createStateObject(initialRoute, route, routes.length === 0); 503 if (routes.length > 0) { 504 nestedState = nestedState.routes[nestedStateIndex].state; 505 } 506 parentScreens.push(route.name); 507 } 508 } 509 route = (0, findFocusedRoute_1.findFocusedRoute)(state); 510 // Remove groups from the path while preserving a trailing slash. 511 route.path = (0, matchers_1.stripGroupSegmentsFromPath)(path); 512 const params = parseQueryParams(route.path, findParseConfigForRoute(route.name, routeConfigs)); 513 if (params) { 514 const resolvedParams = { ...route.params, ...params }; 515 if (Object.keys(resolvedParams).length > 0) { 516 route.params = resolvedParams; 517 } 518 else { 519 delete route.params; 520 } 521 } 522 return state; 523}; 524const parseQueryParams = (path, parseConfig) => { 525 const query = path.split('?')[1]; 526 const params = queryString.parse(query); 527 if (parseConfig) { 528 Object.keys(params).forEach((name) => { 529 if (Object.hasOwnProperty.call(parseConfig, name) && typeof params[name] === 'string') { 530 params[name] = parseConfig[name](params[name]); 531 } 532 }); 533 } 534 return Object.keys(params).length ? params : undefined; 535}; 536const basePathCache = new Map(); 537function getBasePathRegex(basePath) { 538 if (basePathCache.has(basePath)) { 539 return basePathCache.get(basePath); 540 } 541 const regex = new RegExp(`^\\/?${(0, escape_string_regexp_1.default)(basePath)}`, 'g'); 542 basePathCache.set(basePath, regex); 543 return regex; 544} 545function stripBasePath(path, basePath = expo_constants_1.default.expoConfig?.experiments?.basePath) { 546 if (process.env.NODE_ENV !== 'development') { 547 if (basePath) { 548 const reg = getBasePathRegex(basePath); 549 return path.replace(/^\/+/g, '/').replace(reg, ''); 550 } 551 } 552 return path; 553} 554exports.stripBasePath = stripBasePath; 555//# sourceMappingURL=getStateFromPath.js.map