Skip to content

Commit 57509ac

Browse files
committed
fix getStateFromPath - first working solution
1 parent 217d22b commit 57509ac

1 file changed

Lines changed: 359 additions & 0 deletions

File tree

Lines changed: 359 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,359 @@
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..e296919 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,12 @@ function getConfigsWithRegexes(configs) {
148+
regex: c.regex ? new RegExp(c.regex.source + '$') : undefined
149+
}));
150+
}
151+
+const joinPaths = function () {
152+
+ for (var _len = arguments.length, paths = new Array(_len), _key = 0; _key < _len; _key++) {
153+
+ paths[_key] = arguments[_key];
154+
+ }
155+
+ return [].concat(...paths.map(p => p.split('/'))).filter(Boolean).join('/');
156+
+};
157+
const matchAgainstConfigs = (remaining, configs) => {
158+
let routes;
159+
let remainingPath = remaining;
160+
@@ -254,37 +328,34 @@ const matchAgainstConfigs = (remaining, configs) => {
161+
162+
// If our regex matches, we need to extract params from the path
163+
if (match) {
164+
- routes = config.routeNames.map(routeName => {
165+
- const routeConfig = configs.find(c => {
166+
- // Check matching name AND pattern in case same screen is used at different levels in config
167+
- return c.screen === routeName && arrayStartsWith(config.segments, c.segments);
168+
- });
169+
- const params = routeConfig && match.groups ? Object.fromEntries(Object.entries(match.groups).map(([key, value]) => {
170+
- const index = Number(key.replace('param_', ''));
171+
- const param = routeConfig.params.find(it => it.index === index);
172+
- if (param?.screen === routeName && param?.name) {
173+
- return [param.name, value];
174+
- }
175+
- return null;
176+
- }).filter(it => it != null).map(([key, value]) => {
177+
- if (value == null) {
178+
- return [key, undefined];
179+
+ var _config$pattern;
180+
+ 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, {
181+
+ // The param segments appear every second item starting from 2 in the regex match result
182+
+ [p]: match[(i + 1) * 2].replace(/\//, '')
183+
+ }), {});
184+
+ routes = config.routeNames.map(name => {
185+
+ var _config$path;
186+
+ const config = configs.find(c => c.screen === name);
187+
+ 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) => {
188+
+ const value = matchedParams[p];
189+
+ if (value) {
190+
+ var _config$parse;
191+
+ const key = p.replace(/^:/, '').replace(/\?$/, '');
192+
+ acc[key] = (_config$parse = config.parse) !== null && _config$parse !== void 0 && _config$parse[key] ? config.parse[key](value) : value;
193+
}
194+
- const decoded = decodeURIComponent(value);
195+
- const parsed = routeConfig.parse?.[key] ? routeConfig.parse[key](decoded) : decoded;
196+
- return [key, parsed];
197+
- })) : undefined;
198+
+ return acc;
199+
+ }, {});
200+
if (params && Object.keys(params).length) {
201+
return {
202+
- name: routeName,
203+
+ name,
204+
params
205+
};
206+
}
207+
return {
208+
- name: routeName
209+
+ name
210+
};
211+
});
212+
- remainingPath = remainingPath.replace(match[0], '');
213+
+ remainingPath = remainingPath.replace(match[1], '');
214+
break;
215+
}
216+
}
217+
@@ -293,61 +364,34 @@ const matchAgainstConfigs = (remaining, configs) => {
218+
remainingPath
219+
};
220+
};
221+
-const createNormalizedConfigs = (screen, routeConfig, initials, paths, parentScreens, routeNames) => {
222+
+
223+
+const createNormalizedConfigs = function (screen, routeConfig) {
224+
+ let routeNames = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : [];
225+
+ let initials = arguments.length > 3 ? arguments[3] : undefined;
226+
+ let parentScreens = arguments.length > 4 ? arguments[4] : undefined;
227+
+ let parentPattern = arguments.length > 5 ? arguments[5] : undefined;
228+
const configs = [];
229+
routeNames.push(screen);
230+
parentScreens.push(screen);
231+
+
232+
+ // @ts-expect-error: we can't strongly typecheck this for now
233+
const config = routeConfig[screen];
234+
if (typeof config === 'string') {
235+
- paths.push({
236+
- screen,
237+
- path: config
238+
- });
239+
- configs.push(createConfigItem(screen, [...routeNames], [...paths]));
240+
+ // If a string is specified as the value of the key(e.g. Foo: '/path'), use it as the pattern
241+
+ const pattern = parentPattern ? joinPaths(parentPattern, config) : config;
242+
+ configs.push(createConfigItem(screen, routeNames, pattern, config));
243+
} else if (typeof config === 'object') {
244+
+ let pattern;
245+
+
246+
// if an object is specified as the value (e.g. Foo: { ... }),
247+
// it can have `path` property and
248+
// it could have `screens` prop which has nested configs
249+
if (typeof config.path === 'string') {
250+
- if (config.exact && config.path == null) {
251+
- 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: ''\`.`);
252+
+ if (config.exact && config.path === undefined) {
253+
+ 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: ''`.");
254+
}
255+
-
256+
- // We should add alias configs after the main config
257+
- // So unless they are more specific, main config will be matched first
258+
- const aliasConfigs = [];
259+
- if (config.alias) {
260+
- for (const alias of config.alias) {
261+
- if (typeof alias === 'string') {
262+
- aliasConfigs.push(createConfigItem(screen, [...routeNames], [...paths, {
263+
- screen,
264+
- path: alias
265+
- }], config.parse));
266+
- } else if (typeof alias === 'object') {
267+
- aliasConfigs.push(createConfigItem(screen, [...routeNames], alias.exact ? [{
268+
- screen,
269+
- path: alias.path
270+
- }] : [...paths, {
271+
- screen,
272+
- path: alias.path
273+
- }], alias.parse));
274+
- }
275+
- }
276+
- }
277+
- if (config.exact) {
278+
- // If it's an exact path, we don't need to keep track of the parent screens
279+
- // So we can clear it
280+
- paths.length = 0;
281+
- }
282+
- paths.push({
283+
- screen,
284+
- path: config.path
285+
- });
286+
- configs.push(createConfigItem(screen, [...routeNames], [...paths], config.parse));
287+
- configs.push(...aliasConfigs);
288+
- }
289+
- if (typeof config !== 'string' && typeof config.path !== 'string' && config.alias?.length) {
290+
- throw new Error(`Screen '${screen}' doesn't specify a 'path'. A 'path' needs to be specified in order to use 'alias'.`);
291+
+ pattern = config.exact !== true ? joinPaths(parentPattern || '', config.path || '') : config.path || '';
292+
+ configs.push(createConfigItem(screen, routeNames, pattern, config.path, config.parse));
293+
}
294+
if (config.screens) {
295+
// property `initialRouteName` without `screens` has no purpose
296+
@@ -358,7 +402,7 @@ const createNormalizedConfigs = (screen, routeConfig, initials, paths, parentScr
297+
});
298+
}
299+
Object.keys(config.screens).forEach(nestedConfig => {
300+
- const result = createNormalizedConfigs(nestedConfig, config.screens, initials, [...paths], [...parentScreens], routeNames);
301+
+ const result = createNormalizedConfigs(nestedConfig, config.screens, routeNames, initials, [...parentScreens], pattern ?? parentPattern);
302+
configs.push(...result);
303+
});
304+
}
305+
@@ -366,41 +410,27 @@ const createNormalizedConfigs = (screen, routeConfig, initials, paths, parentScr
306+
routeNames.pop();
307+
return configs;
308+
};
309+
-const createConfigItem = (screen, routeNames, paths, parse) => {
310+
- const parts = [];
311+
312+
- // Parse the path string into parts for easier matching
313+
- for (const {
314+
- screen,
315+
- path
316+
- } of paths) {
317+
- parts.push(...getPatternParts(path).map(part => ({
318+
- ...part,
319+
- screen
320+
- })));
321+
- }
322+
- const regex = parts.length ? new RegExp(`^(${parts.map((it, i) => {
323+
- if (it.param) {
324+
- const reg = it.regex || '[^/]+';
325+
- return `(((?<param_${i}>${reg})\\/)${it.optional ? '?' : ''})`;
326+
+const createConfigItem = (screen, routeNames, pattern, path, parse) => {
327+
+ // Normalize pattern to remove any leading, trailing slashes, duplicate slashes etc.
328+
+ pattern = pattern.split('/').filter(Boolean).join('/');
329+
+ const regex = pattern ? new RegExp(`^(${pattern.split('/').map(it => {
330+
+ if (it.startsWith(':')) {
331+
+ return `(([^/]+\\/)${it.endsWith('?') ? '?' : ''})`;
332+
}
333+
- return `${it.segment === '*' ? '.*' : escape(it.segment)}\\/`;
334+
- }).join('')})$`) : undefined;
335+
- const segments = parts.map(it => it.segment);
336+
- const params = parts.map((it, i) => it.param ? {
337+
- index: i,
338+
- screen: it.screen,
339+
- name: it.param
340+
- } : null).filter(it => it != null);
341+
+ return `${it === '*' ? '.*' : escape(it)}\\/`;
342+
+ }).join('')})`) : undefined;
343+
return {
344+
screen,
345+
regex,
346+
- segments,
347+
- params,
348+
- routeNames,
349+
+ pattern,
350+
+ path,
351+
+ // The routeNames array is mutated, so copy it to keep the current state
352+
+ routeNames: [...routeNames],
353+
parse
354+
};
355+
};
356+
+
357+
const findParseConfigForRoute = (routeName, flatConfig) => {
358+
for (const config of flatConfig) {
359+
if (routeName === config.routeNames[config.routeNames.length - 1]) {

0 commit comments

Comments
 (0)