Skip to content

Commit c1258c1

Browse files
committed
Merge branch 'bump/react-navigation-7' of https://github.com/software-mansion-labs/expensify-app-fork into bump/react-navigation-7
2 parents 9b25ca8 + 4da9fa0 commit c1258c1

2 files changed

Lines changed: 368 additions & 0 deletions

File tree

Lines changed: 361 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,361 @@
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]) {

patches/react-navigation/details.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,10 @@
4646
- E/App issue: [#22372](https://github.com/Expensify/App/issues/22372)
4747
- PR Introducing Patch: [#22437](https://github.com/Expensify/App/pull/22437)
4848
- PR Updating Patch: [#33280](https://github.com/Expensify/App/pull/33280) [#37421](https://github.com/Expensify/App/pull/37421) [#49539](https://github.com/Expensify/App/pull/49539) [#64155](https://github.com/Expensify/App/pull/64155)
49+
50+
### [@react-navigation+core+7.10.0+002+getStateFromPath.patch](@react-navigation+core+7.10.0+002+getStateFromPath.patch)
51+
- Reason: Make sure navigation state props retrieved from the path are available at all nesting levels to avoid undefined state.
52+
- Upstream PR/issue: N/A
53+
- E/App issue: [#48150](https://github.com/Expensify/App/issues/48150)
54+
- PR Introducing Patch: [#48151](https://github.com/Expensify/App/pull/48151)
55+
- PR Updating Patch: [#64155](https://github.com/Expensify/App/pull/64155)

0 commit comments

Comments
 (0)