|
| 1 | +diff --git a/node_modules/@react-navigation/core/lib/module/getStateFromPath.js b/node_modules/@react-navigation/core/lib/module/getStateFromPath.js |
| 2 | +index 7132844..8af0a15 100644 |
| 3 | +--- a/node_modules/@react-navigation/core/lib/module/getStateFromPath.js |
| 4 | ++++ b/node_modules/@react-navigation/core/lib/module/getStateFromPath.js |
| 5 | +@@ -29,31 +29,23 @@ import { validatePathConfig } from "./validatePathConfig.js"; |
| 6 | + * @param options Extra options to fine-tune how to parse the path. |
| 7 | + */ |
| 8 | + export function getStateFromPath(path, options) { |
| 9 | +- const { |
| 10 | +- initialRoutes, |
| 11 | +- configs |
| 12 | +- } = getConfigResources(options); |
| 13 | +- const screens = options?.screens; |
| 14 | ++ if (options) { |
| 15 | ++ validatePathConfig(options); |
| 16 | ++ } |
| 17 | ++ let initialRoutes = []; |
| 18 | ++ if (options !== null && options !== void 0 && options.initialRouteName) { |
| 19 | ++ initialRoutes.push({ |
| 20 | ++ initialRouteName: options.initialRouteName, |
| 21 | ++ parentScreens: [] |
| 22 | ++ }); |
| 23 | ++ } |
| 24 | ++ const screens = options === null || options === void 0 ? void 0 : options.screens; |
| 25 | + let remaining = path.replace(/\/+/g, '/') // Replace multiple slash (//) with single ones |
| 26 | +- .replace(/^\//, '') // Remove extra leading slash |
| 27 | +- .replace(/\?.*$/, ''); // Remove query params which we will handle later |
| 28 | ++ .replace(/^\//, '') // Remove extra leading slash |
| 29 | ++ .replace(/\?.*$/, ''); // Remove query params which we will handle later |
| 30 | + |
| 31 | + // Make sure there is a trailing slash |
| 32 | + remaining = remaining.endsWith('/') ? remaining : `${remaining}/`; |
| 33 | +- const prefix = options?.path?.replace(/^\//, ''); // Remove extra leading slash |
| 34 | +- |
| 35 | +- if (prefix) { |
| 36 | +- // Make sure there is a trailing slash |
| 37 | +- const normalizedPrefix = prefix.endsWith('/') ? prefix : `${prefix}/`; |
| 38 | +- |
| 39 | +- // If the path doesn't start with the prefix, it's not a match |
| 40 | +- if (!remaining.startsWith(normalizedPrefix)) { |
| 41 | +- return undefined; |
| 42 | +- } |
| 43 | +- |
| 44 | +- // Remove the prefix from the path |
| 45 | +- remaining = remaining.replace(normalizedPrefix, ''); |
| 46 | +- } |
| 47 | + if (screens === undefined) { |
| 48 | + // When no config is specified, use the path segments as route names |
| 49 | + const routes = remaining.split('/').filter(Boolean).map(segment => { |
| 50 | +@@ -67,10 +59,82 @@ export function getStateFromPath(path, options) { |
| 51 | + } |
| 52 | + return undefined; |
| 53 | + } |
| 54 | ++ |
| 55 | ++ // Create a normalized configs array which will be easier to use |
| 56 | ++ const configs = [].concat(...Object.keys(screens).map(key => createNormalizedConfigs(key, screens, [], initialRoutes, []))).sort((a, b) => { |
| 57 | ++ // Sort config so that: |
| 58 | ++ // - the most exhaustive ones are always at the beginning |
| 59 | ++ // - patterns with wildcard are always at the end |
| 60 | ++ |
| 61 | ++ // If 2 patterns are same, move the one with less route names up |
| 62 | ++ // This is an error state, so it's only useful for consistent error messages |
| 63 | ++ if (a.pattern === b.pattern) { |
| 64 | ++ return b.routeNames.join('>').localeCompare(a.routeNames.join('>')); |
| 65 | ++ } |
| 66 | ++ |
| 67 | ++ // If one of the patterns starts with the other, it's more exhaustive |
| 68 | ++ // So move it up |
| 69 | ++ if (a.pattern.startsWith(b.pattern)) { |
| 70 | ++ return -1; |
| 71 | ++ } |
| 72 | ++ if (b.pattern.startsWith(a.pattern)) { |
| 73 | ++ return 1; |
| 74 | ++ } |
| 75 | ++ const aParts = a.pattern.split('/'); |
| 76 | ++ const bParts = b.pattern.split('/'); |
| 77 | ++ for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) { |
| 78 | ++ // if b is longer, b get higher priority |
| 79 | ++ if (aParts[i] == null) { |
| 80 | ++ return 1; |
| 81 | ++ } |
| 82 | ++ // if a is longer, a get higher priority |
| 83 | ++ if (bParts[i] == null) { |
| 84 | ++ return -1; |
| 85 | ++ } |
| 86 | ++ const aWildCard = aParts[i] === '*' || aParts[i].startsWith(':'); |
| 87 | ++ const bWildCard = bParts[i] === '*' || bParts[i].startsWith(':'); |
| 88 | ++ // if both are wildcard we compare next component |
| 89 | ++ if (aWildCard && bWildCard) { |
| 90 | ++ continue; |
| 91 | ++ } |
| 92 | ++ // if only a is wild card, b get higher priority |
| 93 | ++ if (aWildCard) { |
| 94 | ++ return 1; |
| 95 | ++ } |
| 96 | ++ // if only b is wild card, a get higher priority |
| 97 | ++ if (bWildCard) { |
| 98 | ++ return -1; |
| 99 | ++ } |
| 100 | ++ } |
| 101 | ++ return bParts.length - aParts.length; |
| 102 | ++ }); |
| 103 | ++ |
| 104 | ++ // Check for duplicate patterns in the config |
| 105 | ++ configs.reduce((acc, config) => { |
| 106 | ++ if (acc[config.pattern]) { |
| 107 | ++ const a = acc[config.pattern].routeNames; |
| 108 | ++ const b = config.routeNames; |
| 109 | ++ |
| 110 | ++ // It's not a problem if the path string omitted from a inner most screen |
| 111 | ++ // For example, it's ok if a path resolves to `A > B > C` or `A > B` |
| 112 | ++ const intersects = a.length > b.length ? b.every((it, i) => a[i] === it) : a.every((it, i) => b[i] === it); |
| 113 | ++ if (!intersects) { |
| 114 | ++ 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.`); |
| 115 | ++ } |
| 116 | ++ } |
| 117 | ++ return Object.assign(acc, { |
| 118 | ++ [config.pattern]: config |
| 119 | ++ }); |
| 120 | ++ }, {}); |
| 121 | + if (remaining === '/') { |
| 122 | + // We need to add special handling of empty path so navigation to empty path also works |
| 123 | + // When handling empty path, we should only look at the root level config |
| 124 | +- const match = configs.find(config => config.segments.join('/') === ''); |
| 125 | ++ const match = configs.find(config => config.path === '' && config.routeNames.every( |
| 126 | ++ // Make sure that none of the parent configs have a non-empty path defined |
| 127 | ++ name => { |
| 128 | ++ var _configs$find; |
| 129 | ++ return !((_configs$find = configs.find(c => c.screen === name)) !== null && _configs$find !== void 0 && _configs$find.path); |
| 130 | ++ })); |
| 131 | + if (match) { |
| 132 | + return createNestedStateObject(path, match.routeNames.map(name => ({ |
| 133 | + name |
| 134 | +@@ -86,7 +150,11 @@ export function getStateFromPath(path, options) { |
| 135 | + const { |
| 136 | + routes, |
| 137 | + remainingPath |
| 138 | +- } = matchAgainstConfigs(remaining, configs); |
| 139 | ++ } = matchAgainstConfigs(remaining, configs.map(c => ({ |
| 140 | ++ ...c, |
| 141 | ++ // Add `$` to the regex to make sure it matches till end of the path and not just beginning |
| 142 | ++ regex: c.regex ? new RegExp(c.regex.source + '$') : undefined |
| 143 | ++ }))); |
| 144 | + if (routes !== undefined) { |
| 145 | + // This will always be empty if full path matched |
| 146 | + current = createNestedStateObject(path, routes, initialRoutes, configs); |
| 147 | +@@ -241,6 +309,14 @@ function getConfigsWithRegexes(configs) { |
| 148 | + regex: c.regex ? new RegExp(c.regex.source + '$') : undefined |
| 149 | + })); |
| 150 | + } |
| 151 | ++ |
| 152 | ++const joinPaths = function () { |
| 153 | ++ for (var _len = arguments.length, paths = new Array(_len), _key = 0; _key < _len; _key++) { |
| 154 | ++ paths[_key] = arguments[_key]; |
| 155 | ++ } |
| 156 | ++ return [].concat(...paths.map(p => p.split('/'))).filter(Boolean).join('/'); |
| 157 | ++}; |
| 158 | ++ |
| 159 | + const matchAgainstConfigs = (remaining, configs) => { |
| 160 | + let routes; |
| 161 | + let remainingPath = remaining; |
| 162 | +@@ -254,37 +330,34 @@ const matchAgainstConfigs = (remaining, configs) => { |
| 163 | + |
| 164 | + // If our regex matches, we need to extract params from the path |
| 165 | + if (match) { |
| 166 | +- routes = config.routeNames.map(routeName => { |
| 167 | +- const routeConfig = configs.find(c => { |
| 168 | +- // Check matching name AND pattern in case same screen is used at different levels in config |
| 169 | +- return c.screen === routeName && arrayStartsWith(config.segments, c.segments); |
| 170 | +- }); |
| 171 | +- const params = routeConfig && match.groups ? Object.fromEntries(Object.entries(match.groups).map(([key, value]) => { |
| 172 | +- const index = Number(key.replace('param_', '')); |
| 173 | +- const param = routeConfig.params.find(it => it.index === index); |
| 174 | +- if (param?.screen === routeName && param?.name) { |
| 175 | +- return [param.name, value]; |
| 176 | +- } |
| 177 | +- return null; |
| 178 | +- }).filter(it => it != null).map(([key, value]) => { |
| 179 | +- if (value == null) { |
| 180 | +- return [key, undefined]; |
| 181 | ++ var _config$pattern; |
| 182 | ++ const matchedParams = (_config$pattern = config.pattern) === null || _config$pattern === void 0 ? void 0 : _config$pattern.split('/').filter(p => p.startsWith(':')).reduce((acc, p, i) => Object.assign(acc, { |
| 183 | ++ // The param segments appear every second item starting from 2 in the regex match result |
| 184 | ++ [p]: match[(i + 1) * 2].replace(/\//, '') |
| 185 | ++ }), {}); |
| 186 | ++ routes = config.routeNames.map(name => { |
| 187 | ++ var _config$path; |
| 188 | ++ const config = configs.find(c => c.screen === name); |
| 189 | ++ const params = config === null || config === void 0 ? void 0 : (_config$path = config.path) === null || _config$path === void 0 ? void 0 : _config$path.split('/').filter(p => p.startsWith(':')).reduce((acc, p) => { |
| 190 | ++ const value = matchedParams[p]; |
| 191 | ++ if (value) { |
| 192 | ++ var _config$parse; |
| 193 | ++ const key = p.replace(/^:/, '').replace(/\?$/, ''); |
| 194 | ++ acc[key] = (_config$parse = config.parse) !== null && _config$parse !== void 0 && _config$parse[key] ? config.parse[key](value) : value; |
| 195 | + } |
| 196 | +- const decoded = decodeURIComponent(value); |
| 197 | +- const parsed = routeConfig.parse?.[key] ? routeConfig.parse[key](decoded) : decoded; |
| 198 | +- return [key, parsed]; |
| 199 | +- })) : undefined; |
| 200 | ++ return acc; |
| 201 | ++ }, {}); |
| 202 | + if (params && Object.keys(params).length) { |
| 203 | + return { |
| 204 | +- name: routeName, |
| 205 | ++ name, |
| 206 | + params |
| 207 | + }; |
| 208 | + } |
| 209 | + return { |
| 210 | +- name: routeName |
| 211 | ++ name |
| 212 | + }; |
| 213 | + }); |
| 214 | +- remainingPath = remainingPath.replace(match[0], ''); |
| 215 | ++ remainingPath = remainingPath.replace(match[1], ''); |
| 216 | + break; |
| 217 | + } |
| 218 | + } |
| 219 | +@@ -293,61 +366,34 @@ const matchAgainstConfigs = (remaining, configs) => { |
| 220 | + remainingPath |
| 221 | + }; |
| 222 | + }; |
| 223 | +-const createNormalizedConfigs = (screen, routeConfig, initials, paths, parentScreens, routeNames) => { |
| 224 | ++ |
| 225 | ++const createNormalizedConfigs = function (screen, routeConfig) { |
| 226 | ++ let routeNames = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : []; |
| 227 | ++ let initials = arguments.length > 3 ? arguments[3] : undefined; |
| 228 | ++ let parentScreens = arguments.length > 4 ? arguments[4] : undefined; |
| 229 | ++ let parentPattern = arguments.length > 5 ? arguments[5] : undefined; |
| 230 | + const configs = []; |
| 231 | + routeNames.push(screen); |
| 232 | + parentScreens.push(screen); |
| 233 | ++ |
| 234 | ++ // @ts-expect-error: we can't strongly typecheck this for now |
| 235 | + const config = routeConfig[screen]; |
| 236 | + if (typeof config === 'string') { |
| 237 | +- paths.push({ |
| 238 | +- screen, |
| 239 | +- path: config |
| 240 | +- }); |
| 241 | +- configs.push(createConfigItem(screen, [...routeNames], [...paths])); |
| 242 | ++ // If a string is specified as the value of the key(e.g. Foo: '/path'), use it as the pattern |
| 243 | ++ const pattern = parentPattern ? joinPaths(parentPattern, config) : config; |
| 244 | ++ configs.push(createConfigItem(screen, routeNames, pattern, config)); |
| 245 | + } else if (typeof config === 'object') { |
| 246 | ++ let pattern; |
| 247 | ++ |
| 248 | + // if an object is specified as the value (e.g. Foo: { ... }), |
| 249 | + // it can have `path` property and |
| 250 | + // it could have `screens` prop which has nested configs |
| 251 | + if (typeof config.path === 'string') { |
| 252 | +- if (config.exact && config.path == null) { |
| 253 | +- throw new Error(`Screen '${screen}' doesn't specify a 'path'. 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: ''\`.`); |
| 254 | ++ if (config.exact && config.path === undefined) { |
| 255 | ++ 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: ''`."); |
| 256 | + } |
| 257 | +- |
| 258 | +- // We should add alias configs after the main config |
| 259 | +- // So unless they are more specific, main config will be matched first |
| 260 | +- const aliasConfigs = []; |
| 261 | +- if (config.alias) { |
| 262 | +- for (const alias of config.alias) { |
| 263 | +- if (typeof alias === 'string') { |
| 264 | +- aliasConfigs.push(createConfigItem(screen, [...routeNames], [...paths, { |
| 265 | +- screen, |
| 266 | +- path: alias |
| 267 | +- }], config.parse)); |
| 268 | +- } else if (typeof alias === 'object') { |
| 269 | +- aliasConfigs.push(createConfigItem(screen, [...routeNames], alias.exact ? [{ |
| 270 | +- screen, |
| 271 | +- path: alias.path |
| 272 | +- }] : [...paths, { |
| 273 | +- screen, |
| 274 | +- path: alias.path |
| 275 | +- }], alias.parse)); |
| 276 | +- } |
| 277 | +- } |
| 278 | +- } |
| 279 | +- if (config.exact) { |
| 280 | +- // If it's an exact path, we don't need to keep track of the parent screens |
| 281 | +- // So we can clear it |
| 282 | +- paths.length = 0; |
| 283 | +- } |
| 284 | +- paths.push({ |
| 285 | +- screen, |
| 286 | +- path: config.path |
| 287 | +- }); |
| 288 | +- configs.push(createConfigItem(screen, [...routeNames], [...paths], config.parse)); |
| 289 | +- configs.push(...aliasConfigs); |
| 290 | +- } |
| 291 | +- if (typeof config !== 'string' && typeof config.path !== 'string' && config.alias?.length) { |
| 292 | +- throw new Error(`Screen '${screen}' doesn't specify a 'path'. A 'path' needs to be specified in order to use 'alias'.`); |
| 293 | ++ pattern = config.exact !== true ? joinPaths(parentPattern || '', config.path || '') : config.path || ''; |
| 294 | ++ configs.push(createConfigItem(screen, routeNames, pattern, config.path, config.parse)); |
| 295 | + } |
| 296 | + if (config.screens) { |
| 297 | + // property `initialRouteName` without `screens` has no purpose |
| 298 | +@@ -358,7 +404,7 @@ const createNormalizedConfigs = (screen, routeConfig, initials, paths, parentScr |
| 299 | + }); |
| 300 | + } |
| 301 | + Object.keys(config.screens).forEach(nestedConfig => { |
| 302 | +- const result = createNormalizedConfigs(nestedConfig, config.screens, initials, [...paths], [...parentScreens], routeNames); |
| 303 | ++ const result = createNormalizedConfigs(nestedConfig, config.screens, routeNames, initials, [...parentScreens], pattern ?? parentPattern); |
| 304 | + configs.push(...result); |
| 305 | + }); |
| 306 | + } |
| 307 | +@@ -366,41 +412,27 @@ const createNormalizedConfigs = (screen, routeConfig, initials, paths, parentScr |
| 308 | + routeNames.pop(); |
| 309 | + return configs; |
| 310 | + }; |
| 311 | +-const createConfigItem = (screen, routeNames, paths, parse) => { |
| 312 | +- const parts = []; |
| 313 | + |
| 314 | +- // Parse the path string into parts for easier matching |
| 315 | +- for (const { |
| 316 | +- screen, |
| 317 | +- path |
| 318 | +- } of paths) { |
| 319 | +- parts.push(...getPatternParts(path).map(part => ({ |
| 320 | +- ...part, |
| 321 | +- screen |
| 322 | +- }))); |
| 323 | +- } |
| 324 | +- const regex = parts.length ? new RegExp(`^(${parts.map((it, i) => { |
| 325 | +- if (it.param) { |
| 326 | +- const reg = it.regex || '[^/]+'; |
| 327 | +- return `(((?<param_${i}>${reg})\\/)${it.optional ? '?' : ''})`; |
| 328 | ++const createConfigItem = (screen, routeNames, pattern, path, parse) => { |
| 329 | ++ // Normalize pattern to remove any leading, trailing slashes, duplicate slashes etc. |
| 330 | ++ pattern = pattern.split('/').filter(Boolean).join('/'); |
| 331 | ++ const regex = pattern ? new RegExp(`^(${pattern.split('/').map(it => { |
| 332 | ++ if (it.startsWith(':')) { |
| 333 | ++ return `(([^/]+\\/)${it.endsWith('?') ? '?' : ''})`; |
| 334 | + } |
| 335 | +- return `${it.segment === '*' ? '.*' : escape(it.segment)}\\/`; |
| 336 | +- }).join('')})$`) : undefined; |
| 337 | +- const segments = parts.map(it => it.segment); |
| 338 | +- const params = parts.map((it, i) => it.param ? { |
| 339 | +- index: i, |
| 340 | +- screen: it.screen, |
| 341 | +- name: it.param |
| 342 | +- } : null).filter(it => it != null); |
| 343 | ++ return `${it === '*' ? '.*' : escape(it)}\\/`; |
| 344 | ++ }).join('')})`) : undefined; |
| 345 | + return { |
| 346 | + screen, |
| 347 | + regex, |
| 348 | +- segments, |
| 349 | +- params, |
| 350 | +- routeNames, |
| 351 | ++ pattern, |
| 352 | ++ path, |
| 353 | ++ // The routeNames array is mutated, so copy it to keep the current state |
| 354 | ++ routeNames: [...routeNames], |
| 355 | + parse |
| 356 | + }; |
| 357 | + }; |
| 358 | ++ |
| 359 | + const findParseConfigForRoute = (routeName, flatConfig) => { |
| 360 | + for (const config of flatConfig) { |
| 361 | + if (routeName === config.routeNames[config.routeNames.length - 1]) { |
0 commit comments