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.appendBasePath = exports.deepEqual = exports.getPathDataFromState = void 0; 30const core_1 = require("@react-navigation/core"); 31const expo_constants_1 = __importDefault(require("expo-constants")); 32const queryString = __importStar(require("query-string")); 33const matchers_1 = require("../matchers"); 34const DEFAULT_SCREENS = {}; 35const getActiveRoute = (state) => { 36 const route = typeof state.index === 'number' 37 ? state.routes[state.index] 38 : state.routes[state.routes.length - 1]; 39 if (route.state) { 40 return getActiveRoute(route.state); 41 } 42 if (route && isInvalidParams(route.params)) { 43 return getActiveRoute(createFakeState(route.params)); 44 } 45 return route; 46}; 47function createFakeState(params) { 48 return { 49 stale: false, 50 type: 'UNKNOWN', 51 key: 'UNKNOWN', 52 index: 0, 53 routeNames: [], 54 routes: [ 55 { 56 key: 'UNKNOWN', 57 name: params.screen, 58 params: params.params, 59 path: params.path, 60 }, 61 ], 62 }; 63} 64function segmentMatchesConvention(segment) { 65 return (segment === 'index' || 66 (0, matchers_1.matchDynamicName)(segment) != null || 67 (0, matchers_1.matchGroupName)(segment) != null || 68 (0, matchers_1.matchDeepDynamicRouteName)(segment) != null); 69} 70function encodeURIComponentPreservingBrackets(str) { 71 return encodeURIComponent(str).replace(/%5B/g, '[').replace(/%5D/g, ']'); 72} 73/** 74 * Utility to serialize a navigation state object to a path string. 75 * 76 * @example 77 * ```js 78 * getPathFromState( 79 * { 80 * routes: [ 81 * { 82 * name: 'Chat', 83 * params: { author: 'Jane', id: 42 }, 84 * }, 85 * ], 86 * }, 87 * { 88 * screens: { 89 * Chat: { 90 * path: 'chat/:author/:id', 91 * stringify: { author: author => author.toLowerCase() } 92 * } 93 * } 94 * } 95 * ) 96 * ``` 97 * 98 * @param state Navigation state to serialize. 99 * @param options Extra options to fine-tune how to serialize the path. 100 * @returns Path representing the state, e.g. /foo/bar?count=42. 101 */ 102function getPathFromState(state, _options) { 103 return getPathDataFromState(state, _options).path; 104} 105exports.default = getPathFromState; 106function getPathDataFromState(state, _options = { screens: DEFAULT_SCREENS }) { 107 if (state == null) { 108 throw Error("Got 'undefined' for the navigation state. You must pass a valid state object."); 109 } 110 const { preserveGroups, preserveDynamicRoutes, ...options } = _options; 111 (0, core_1.validatePathConfig)(options); 112 // Expo Router disallows usage without a linking config. 113 if (Object.is(options.screens, DEFAULT_SCREENS)) { 114 throw Error("You must pass a 'screens' object to 'getPathFromState' to generate a path."); 115 } 116 return getPathFromResolvedState(JSON.parse(JSON.stringify(state)), 117 // Create a normalized configs object which will be easier to use 118 createNormalizedConfigs(options.screens), { preserveGroups, preserveDynamicRoutes }); 119} 120exports.getPathDataFromState = getPathDataFromState; 121function processParamsWithUserSettings(configItem, params) { 122 const stringify = configItem?.stringify; 123 return Object.fromEntries(Object.entries(params).map(([key, value]) => [ 124 key, 125 // TODO: Strip nullish values here. 126 stringify?.[key] 127 ? stringify[key](value) 128 : // Preserve rest params 129 Array.isArray(value) 130 ? value 131 : String(value), 132 ])); 133} 134function deepEqual(a, b) { 135 if (a === b) { 136 return true; 137 } 138 if (Array.isArray(a) && Array.isArray(b)) { 139 if (a.length !== b.length) { 140 return false; 141 } 142 for (let i = 0; i < a.length; i++) { 143 if (!deepEqual(a[i], b[i])) { 144 return false; 145 } 146 } 147 return true; 148 } 149 if (typeof a === 'object' && typeof b === 'object') { 150 const keysA = Object.keys(a); 151 const keysB = Object.keys(b); 152 if (keysA.length !== keysB.length) { 153 return false; 154 } 155 for (const key of keysA) { 156 if (!deepEqual(a[key], b[key])) { 157 return false; 158 } 159 } 160 return true; 161 } 162 return false; 163} 164exports.deepEqual = deepEqual; 165function walkConfigItems(route, focusedRoute, configs, { preserveDynamicRoutes, }) { 166 // NOTE(EvanBacon): Fill in current route using state that was passed as params. 167 if (!route.state && isInvalidParams(route.params)) { 168 route.state = createFakeState(route.params); 169 } 170 let pattern = null; 171 let focusedParams; 172 const collectedParams = {}; 173 while (route.name in configs) { 174 const configItem = configs[route.name]; 175 const inputPattern = configItem.pattern; 176 if (inputPattern == null) { 177 // This should never happen in Expo Router. 178 throw new Error('Unexpected: No pattern found for route ' + route.name); 179 } 180 pattern = inputPattern; 181 if (route.params) { 182 const params = processParamsWithUserSettings(configItem, route.params); 183 // TODO: Does this need to be a null check? 184 if (pattern) { 185 Object.assign(collectedParams, params); 186 } 187 if (deepEqual(focusedRoute, route)) { 188 if (preserveDynamicRoutes) { 189 focusedParams = params; 190 } 191 else { 192 // If this is the focused route, keep the params for later use 193 // We save it here since it's been stringified already 194 focusedParams = getParamsWithConventionsCollapsed({ 195 params, 196 pattern, 197 routeName: route.name, 198 }); 199 } 200 } 201 } 202 if (!route.state && isInvalidParams(route.params)) { 203 route.state = createFakeState(route.params); 204 } 205 // If there is no `screens` property or no nested state, we return pattern 206 if (!configItem.screens || route.state === undefined) { 207 if (configItem.initialRouteName && 208 configItem.screens && 209 configItem.initialRouteName in configItem.screens && 210 configItem.screens[configItem.initialRouteName]?.pattern) { 211 const initialRouteConfig = configItem.screens[configItem.initialRouteName]; 212 // NOTE(EvanBacon): Big hack to support initial route changes in tab bars. 213 pattern = initialRouteConfig.pattern; 214 if (focusedParams) { 215 if (!preserveDynamicRoutes) { 216 // If this is the focused route, keep the params for later use 217 // We save it here since it's been stringified already 218 focusedParams = getParamsWithConventionsCollapsed({ 219 params: focusedParams, 220 pattern, 221 routeName: route.name, 222 }); 223 } 224 } 225 } 226 break; 227 } 228 const index = route.state.index ?? route.state.routes.length - 1; 229 const nextRoute = route.state.routes[index]; 230 const nestedScreens = configItem.screens; 231 // if there is config for next route name, we go deeper 232 if (nestedScreens && nextRoute.name in nestedScreens) { 233 route = nextRoute; 234 configs = nestedScreens; 235 } 236 else { 237 // If not, there is no sense in going deeper in config 238 break; 239 } 240 } 241 if (pattern == null) { 242 throw new Error(`No pattern found for route "${route.name}". Options are: ${Object.keys(configs).join(', ')}.`); 243 } 244 if (pattern && !focusedParams && focusedRoute.params) { 245 if (preserveDynamicRoutes) { 246 focusedParams = focusedRoute.params; 247 } 248 else { 249 // If this is the focused route, keep the params for later use 250 // We save it here since it's been stringified already 251 focusedParams = getParamsWithConventionsCollapsed({ 252 params: focusedRoute.params, 253 pattern, 254 routeName: route.name, 255 }); 256 } 257 Object.assign(focusedParams, collectedParams); 258 } 259 return { 260 pattern, 261 nextRoute: route, 262 focusedParams, 263 params: collectedParams, 264 }; 265} 266function getPathFromResolvedState(state, configs, { preserveGroups, preserveDynamicRoutes, }) { 267 let path = ''; 268 let current = state; 269 const allParams = {}; 270 while (current) { 271 path += '/'; 272 // Make mutable copies to ensure we don't leak state outside of the function. 273 const route = current.routes[current.index ?? 0]; 274 // NOTE(EvanBacon): Fill in current route using state that was passed as params. 275 // if (isInvalidParams(route.params)) { 276 if (!route.state && isInvalidParams(route.params)) { 277 route.state = createFakeState(route.params); 278 } 279 const { pattern, params, nextRoute, focusedParams } = walkConfigItems(route, getActiveRoute(current), { ...configs }, { preserveDynamicRoutes }); 280 Object.assign(allParams, params); 281 path += getPathWithConventionsCollapsed({ 282 pattern, 283 routePath: nextRoute.path, 284 params: allParams, 285 initialRouteName: configs[nextRoute.name]?.initialRouteName, 286 preserveGroups, 287 preserveDynamicRoutes, 288 }); 289 if (nextRoute.state && 290 // NOTE(EvanBacon): The upstream implementation allows for sending in synthetic states (states that weren't generated by `getStateFromPath`) 291 // and any invalid routes will simply be ignored. 292 // Because of this, we need to check if the next route is valid before continuing, otherwise our more strict 293 // implementation will throw an error. 294 configs[nextRoute.state.routes?.[nextRoute.state?.index ?? 0]?.name]) { 295 // Continue looping with the next state if available. 296 current = nextRoute.state; 297 } 298 else { 299 // Finished crawling state. 300 // Check for query params before exiting. 301 if (focusedParams) { 302 for (const param in focusedParams) { 303 // TODO: This is not good. We shouldn't squat strings named "undefined". 304 if (focusedParams[param] === 'undefined') { 305 // eslint-disable-next-line @typescript-eslint/no-dynamic-delete 306 delete focusedParams[param]; 307 } 308 } 309 const query = queryString.stringify(focusedParams, { sort: false }); 310 if (query) { 311 path += `?${query}`; 312 } 313 } 314 break; 315 } 316 } 317 return { path: appendBasePath(basicSanitizePath(path)), params: decodeParams(allParams) }; 318} 319function decodeParams(params) { 320 const parsed = {}; 321 for (const [key, value] of Object.entries(params)) { 322 parsed[key] = decodeURIComponent(value); 323 } 324 return parsed; 325} 326function getPathWithConventionsCollapsed({ pattern, routePath, params, preserveGroups, preserveDynamicRoutes, initialRouteName, }) { 327 const segments = pattern.split('/'); 328 return segments 329 .map((p, i) => { 330 const name = getParamName(p); 331 // We don't know what to show for wildcard patterns 332 // Showing the route name seems ok, though whatever we show here will be incorrect 333 // Since the page doesn't actually exist 334 if (p.startsWith('*')) { 335 if (preserveDynamicRoutes) { 336 return `[...${name}]`; 337 } 338 if (params[name]) { 339 if (Array.isArray(params[name])) { 340 return params[name].join('/'); 341 } 342 return params[name]; 343 } 344 if (i === 0) { 345 // This can occur when a wildcard matches all routes and the given path was `/`. 346 return routePath; 347 } 348 // remove existing segments from route.path and return it 349 // this is used for nested wildcard routes. Without this, the path would add 350 // all nested segments to the beginning of the wildcard route. 351 return routePath 352 ?.split('/') 353 .slice(i + 1) 354 .join('/'); 355 } 356 // If the path has a pattern for a param, put the param in the path 357 if (p.startsWith(':')) { 358 if (preserveDynamicRoutes) { 359 return `[${name}]`; 360 } 361 // Optional params without value assigned in route.params should be ignored 362 return params[name]; 363 } 364 if (!preserveGroups && (0, matchers_1.matchGroupName)(p) != null) { 365 // When the last part is a group it could be a shared URL 366 // if the route has an initialRouteName defined, then we should 367 // use that as the component path as we can assume it will be shown. 368 if (segments.length - 1 === i) { 369 if (initialRouteName) { 370 // Return an empty string if the init route is ambiguous. 371 if (segmentMatchesConvention(initialRouteName)) { 372 return ''; 373 } 374 return encodeURIComponentPreservingBrackets(initialRouteName); 375 } 376 } 377 return ''; 378 } 379 // Preserve dynamic syntax for rehydration 380 return encodeURIComponentPreservingBrackets(p); 381 }) 382 .map((v) => v ?? '') 383 .join('/'); 384} 385/** Given a set of query params and a pattern with possible conventions, collapse the conventions and return the remaining params. */ 386function getParamsWithConventionsCollapsed({ pattern, routeName, params, }) { 387 const processedParams = { ...params }; 388 // Remove the params present in the pattern since we'll only use the rest for query string 389 const segments = pattern.split('/'); 390 // Dynamic Routes 391 segments 392 .filter((segment) => segment.startsWith(':')) 393 .forEach((segment) => { 394 const name = getParamName(segment); 395 delete processedParams[name]; 396 }); 397 // Deep Dynamic Routes 398 if (segments.some((segment) => segment.startsWith('*'))) { 399 // NOTE(EvanBacon): Drop the param name matching the wildcard route name -- this is specific to Expo Router. 400 const name = (0, matchers_1.matchDeepDynamicRouteName)(routeName) ?? routeName; 401 delete processedParams[name]; 402 } 403 return processedParams; 404} 405// Remove multiple as well as trailing slashes 406function basicSanitizePath(path) { 407 // Remove duplicate slashes like `foo//bar` -> `foo/bar` 408 const simplifiedPath = path.replace(/\/+/g, '/'); 409 if (simplifiedPath.length <= 1) { 410 return simplifiedPath; 411 } 412 // Remove trailing slash like `foo/bar/` -> `foo/bar` 413 return simplifiedPath.replace(/\/$/, ''); 414} 415// TODO: Make StackRouter not do this... 416// Detect if the params came from StackRouter using `params` to pass around internal state. 417function isInvalidParams(params) { 418 if (!params) { 419 return false; 420 } 421 if ('params' in params && typeof params.params === 'object' && !!params.params) { 422 return true; 423 } 424 return ('initial' in params && 425 typeof params.initial === 'boolean' && 426 // "path" in params && 427 'screen' in params); 428} 429const getParamName = (pattern) => pattern.replace(/^[:*]/, '').replace(/\?$/, ''); 430const joinPaths = (...paths) => [] 431 .concat(...paths.map((p) => p.split('/'))) 432 .filter(Boolean) 433 .join('/'); 434const createConfigItem = (config, parentPattern) => { 435 if (typeof config === 'string') { 436 // If a string is specified as the value of the key(e.g. Foo: '/path'), use it as the pattern 437 const pattern = parentPattern ? joinPaths(parentPattern, config) : config; 438 return { pattern }; 439 } 440 if (config.exact && config.path === undefined) { 441 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: ''`."); 442 } 443 // If an object is specified as the value (e.g. Foo: { ... }), 444 // It can have `path` property and `screens` prop which has nested configs 445 const pattern = config.exact !== true ? joinPaths(parentPattern || '', config.path || '') : config.path || ''; 446 const screens = config.screens ? createNormalizedConfigs(config.screens, pattern) : undefined; 447 return { 448 // Normalize pattern to remove any leading, trailing slashes, duplicate slashes etc. 449 pattern: pattern?.split('/').filter(Boolean).join('/'), 450 stringify: config.stringify, 451 screens, 452 initialRouteName: config.initialRouteName, 453 }; 454}; 455const createNormalizedConfigs = (options, pattern) => Object.fromEntries(Object.entries(options).map(([name, c]) => [name, createConfigItem(c, pattern)])); 456function appendBasePath(path, assetPrefix = expo_constants_1.default.expoConfig?.experiments?.basePath) { 457 if (process.env.NODE_ENV !== 'development') { 458 if (assetPrefix) { 459 return `/${assetPrefix.replace(/^\/+/, '').replace(/\/$/, '')}${path}`; 460 } 461 } 462 return path; 463} 464exports.appendBasePath = appendBasePath; 465//# sourceMappingURL=getPathFromState.js.map