@react-navigation/core 7.21.4 → 7.21.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/lib/module/BaseNavigationContainer.js +14 -8
  2. package/lib/module/BaseNavigationContainer.js.map +1 -1
  3. package/lib/module/ConsumedParamsContext.js.map +1 -1
  4. package/lib/module/NavigationIndependentTree.js +8 -4
  5. package/lib/module/NavigationIndependentTree.js.map +1 -1
  6. package/lib/module/SceneView.js +16 -39
  7. package/lib/module/SceneView.js.map +1 -1
  8. package/lib/module/StaticNavigation.js +1 -1
  9. package/lib/module/StaticNavigation.js.map +1 -1
  10. package/lib/module/findFocusedRoute.js +3 -3
  11. package/lib/module/findFocusedRoute.js.map +1 -1
  12. package/lib/module/getActionFromState.js.map +1 -1
  13. package/lib/module/getPathFromState.js +75 -45
  14. package/lib/module/getPathFromState.js.map +1 -1
  15. package/lib/module/getPatternParts.js +15 -2
  16. package/lib/module/getPatternParts.js.map +1 -1
  17. package/lib/module/getStateFromPath.js +47 -21
  18. package/lib/module/getStateFromPath.js.map +1 -1
  19. package/lib/module/getStateFromRouteParams.js +24 -0
  20. package/lib/module/getStateFromRouteParams.js.map +1 -0
  21. package/lib/module/useFocusEvents.js +6 -2
  22. package/lib/module/useFocusEvents.js.map +1 -1
  23. package/lib/module/useLazyValue.js.map +1 -1
  24. package/lib/module/useNavigationBuilder.js +9 -6
  25. package/lib/module/useNavigationBuilder.js.map +1 -1
  26. package/lib/module/useNavigationState.js +44 -11
  27. package/lib/module/useNavigationState.js.map +1 -1
  28. package/lib/module/useOnPreventRemove.js +1 -3
  29. package/lib/module/useOnPreventRemove.js.map +1 -1
  30. package/lib/module/useRouteCache.js +1 -3
  31. package/lib/module/useRouteCache.js.map +1 -1
  32. package/lib/module/useSyncState.js +3 -2
  33. package/lib/module/useSyncState.js.map +1 -1
  34. package/lib/typescript/src/BaseNavigationContainer.d.ts.map +1 -1
  35. package/lib/typescript/src/ConsumedParamsContext.d.ts +1 -6
  36. package/lib/typescript/src/ConsumedParamsContext.d.ts.map +1 -1
  37. package/lib/typescript/src/EnsureSingleNavigator.d.ts +1 -1
  38. package/lib/typescript/src/EnsureSingleNavigator.d.ts.map +1 -1
  39. package/lib/typescript/src/NavigationIndependentTree.d.ts +1 -1
  40. package/lib/typescript/src/NavigationIndependentTree.d.ts.map +1 -1
  41. package/lib/typescript/src/NavigationProvider.d.ts +1 -1
  42. package/lib/typescript/src/NavigationProvider.d.ts.map +1 -1
  43. package/lib/typescript/src/PreventRemoveProvider.d.ts +1 -1
  44. package/lib/typescript/src/PreventRemoveProvider.d.ts.map +1 -1
  45. package/lib/typescript/src/SceneView.d.ts +2 -1
  46. package/lib/typescript/src/SceneView.d.ts.map +1 -1
  47. package/lib/typescript/src/StaticContainer.d.ts +1 -1
  48. package/lib/typescript/src/StaticContainer.d.ts.map +1 -1
  49. package/lib/typescript/src/findFocusedRoute.d.ts.map +1 -1
  50. package/lib/typescript/src/getPathFromState.d.ts.map +1 -1
  51. package/lib/typescript/src/getPatternParts.d.ts.map +1 -1
  52. package/lib/typescript/src/getStateFromPath.d.ts.map +1 -1
  53. package/lib/typescript/src/getStateFromRouteParams.d.ts +3 -0
  54. package/lib/typescript/src/getStateFromRouteParams.d.ts.map +1 -0
  55. package/lib/typescript/src/theming/ThemeProvider.d.ts +1 -1
  56. package/lib/typescript/src/theming/ThemeProvider.d.ts.map +1 -1
  57. package/lib/typescript/src/types.d.ts +15 -9
  58. package/lib/typescript/src/types.d.ts.map +1 -1
  59. package/lib/typescript/src/useComponent.d.ts +1 -1
  60. package/lib/typescript/src/useComponent.d.ts.map +1 -1
  61. package/lib/typescript/src/useFocusEvents.d.ts.map +1 -1
  62. package/lib/typescript/src/useLazyValue.d.ts.map +1 -1
  63. package/lib/typescript/src/useNavigationBuilder.d.ts +1 -1
  64. package/lib/typescript/src/useNavigationBuilder.d.ts.map +1 -1
  65. package/lib/typescript/src/useNavigationState.d.ts +3 -2
  66. package/lib/typescript/src/useNavigationState.d.ts.map +1 -1
  67. package/lib/typescript/src/useOnPreventRemove.d.ts.map +1 -1
  68. package/lib/typescript/src/useRouteCache.d.ts.map +1 -1
  69. package/lib/typescript/src/useSyncState.d.ts.map +1 -1
  70. package/package.json +7 -7
  71. package/src/BaseNavigationContainer.tsx +16 -10
  72. package/src/ConsumedParamsContext.tsx +1 -6
  73. package/src/NavigationIndependentTree.tsx +8 -5
  74. package/src/SceneView.tsx +18 -51
  75. package/src/StaticNavigation.tsx +2 -2
  76. package/src/findFocusedRoute.tsx +5 -3
  77. package/src/getActionFromState.tsx +3 -1
  78. package/src/getPathFromState.tsx +130 -59
  79. package/src/getPatternParts.tsx +15 -2
  80. package/src/getStateFromPath.tsx +76 -17
  81. package/src/getStateFromRouteParams.tsx +60 -0
  82. package/src/types.tsx +42 -24
  83. package/src/useDescriptors.tsx +1 -1
  84. package/src/useFocusEvents.tsx +7 -5
  85. package/src/useLazyValue.tsx +2 -2
  86. package/src/useNavigationBuilder.tsx +16 -8
  87. package/src/useNavigationState.tsx +51 -21
  88. package/src/useOnPreventRemove.tsx +4 -3
  89. package/src/useRouteCache.tsx +1 -4
  90. package/src/useSyncState.tsx +3 -2
@@ -1,11 +1,8 @@
1
- import type {
2
- NavigationState,
3
- PartialState,
4
- Route,
5
- } from '@react-navigation/routers';
1
+ import type { NavigationState, PartialState } from '@react-navigation/routers';
6
2
  import * as queryString from 'query-string';
7
3
 
8
4
  import { getPatternParts, type PatternPart } from './getPatternParts';
5
+ import { getStateFromRouteParams } from './getStateFromRouteParams';
9
6
  import type { PathConfig, PathConfigMap } from './types';
10
7
  import { validatePathConfig } from './validatePathConfig';
11
8
 
@@ -19,20 +16,40 @@ type State = NavigationState | Omit<PartialState<NavigationState>, 'stale'>;
19
16
 
20
17
  type StringifyConfig = Record<string, (value: unknown) => string>;
21
18
 
19
+ type SerializedParamValue = string | string[] | null;
20
+
22
21
  type ConfigItem = {
23
22
  parts?: PatternPart[];
23
+ ownParts: PatternPart[];
24
24
  stringify?: StringifyConfig;
25
25
  screens?: Record<string, ConfigItem>;
26
26
  };
27
27
 
28
- const getActiveRoute = (state: State): { name: string; params?: object } => {
28
+ const serializeParamValue = (value: unknown): SerializedParamValue =>
29
+ value === null
30
+ ? null
31
+ : Array.isArray(value)
32
+ ? value.map(String)
33
+ : String(value);
34
+
35
+ const getActiveRoute = (
36
+ state: State,
37
+ configs: Record<string, ConfigItem> | undefined,
38
+ getRouteState: (
39
+ route: State['routes'][number],
40
+ config: ConfigItem | undefined
41
+ ) => State | undefined
42
+ ): { name: string; params?: object } => {
29
43
  const route =
30
44
  typeof state.index === 'number'
31
45
  ? state.routes[state.index]
32
46
  : state.routes[state.routes.length - 1];
33
47
 
34
- if (route.state) {
35
- return getActiveRoute(route.state);
48
+ const config = configs?.[route.name];
49
+ const routeState = getRouteState(route, config);
50
+
51
+ if (routeState) {
52
+ return getActiveRoute(routeState, config?.screens, getRouteState);
36
53
  }
37
54
 
38
55
  return route;
@@ -101,24 +118,55 @@ export function getPathFromState<ParamList extends {}>(
101
118
  }
102
119
 
103
120
  const configs = getNormalizedConfigs(options);
121
+ const cachedRouteStates = new Map<object, State | undefined>();
122
+
123
+ const getRouteState = (
124
+ route: State['routes'][number],
125
+ config: ConfigItem | undefined
126
+ ) => {
127
+ if (route.state) {
128
+ return route.state;
129
+ }
130
+
131
+ const hasScreenParams =
132
+ route.params &&
133
+ 'screen' in route.params &&
134
+ typeof route.params.screen === 'string' &&
135
+ config?.screens?.[route.params.screen];
136
+
137
+ const hasStateParams =
138
+ route.params && 'state' in route.params && config?.screens;
139
+
140
+ if (
141
+ !route.params ||
142
+ !config?.screens ||
143
+ (!hasScreenParams && !hasStateParams)
144
+ ) {
145
+ return undefined;
146
+ }
147
+
148
+ if (!cachedRouteStates.has(route)) {
149
+ cachedRouteStates.set(route, getStateFromRouteParams(route.params));
150
+ }
151
+
152
+ return cachedRouteStates.get(route);
153
+ };
154
+
155
+ const focusedRoute = getActiveRoute(state, configs, getRouteState);
104
156
 
105
157
  let path = '/';
106
158
  let current: State | undefined = state;
107
159
 
108
- const allParams: Record<string, string> = {};
109
-
110
160
  while (current) {
111
161
  let index = typeof current.index === 'number' ? current.index : 0;
112
- let route = current.routes[index] as Route<string> & {
113
- state?: State;
114
- };
162
+ let route = current.routes[index];
115
163
 
116
164
  let parts: PatternPart[] | undefined;
117
165
 
118
- let focusedParams: Record<string, string> | undefined;
119
- let currentOptions = configs;
166
+ const partValues = new Map<PatternPart, SerializedParamValue | undefined>();
120
167
 
121
- const focusedRoute = getActiveRoute(state);
168
+ let focusedParams: Record<string, SerializedParamValue> | undefined;
169
+ let currentOptions = configs;
122
170
 
123
171
  // Keep all the route names that appeared during going deeper in config in case the pattern is resolved to undefined
124
172
  const nestedRouteNames = [];
@@ -126,19 +174,21 @@ export function getPathFromState<ParamList extends {}>(
126
174
  let hasNext = true;
127
175
 
128
176
  while (route.name in currentOptions && hasNext) {
129
- parts = currentOptions[route.name].parts;
177
+ const config = currentOptions[route.name];
178
+ parts = config.parts;
179
+ const ownParts = config.ownParts;
130
180
 
131
181
  nestedRouteNames.push(route.name);
132
182
 
133
183
  if (route.params) {
134
- const options = currentOptions[route.name];
184
+ const options = config;
135
185
 
136
186
  const currentParams = Object.fromEntries(
137
187
  Object.entries(route.params)
138
- .map(([key, value]): [string, string] | null => {
188
+ .map(([key, value]): [string, SerializedParamValue] | null => {
139
189
  if (value === undefined) {
140
190
  if (options) {
141
- const optional = options.parts?.find(
191
+ const optional = ownParts.find(
142
192
  (part) => part.param === key
143
193
  )?.optional;
144
194
 
@@ -150,51 +200,71 @@ export function getPathFromState<ParamList extends {}>(
150
200
  }
151
201
  }
152
202
 
153
- const stringify = options?.stringify?.[key] ?? String;
203
+ const stringify = options?.stringify?.[key];
154
204
 
155
- return [key, stringify(value)];
205
+ return [
206
+ key,
207
+ stringify ? stringify(value) : serializeParamValue(value),
208
+ ];
156
209
  })
157
210
  .filter((entry) => entry != null)
158
211
  );
159
212
 
160
- if (parts?.length) {
161
- Object.assign(allParams, currentParams);
213
+ const claimedParams = new Set<string>();
214
+
215
+ for (const part of ownParts) {
216
+ if (part.param && part.param in currentParams) {
217
+ const value = currentParams[part.param];
218
+
219
+ if (value !== undefined) {
220
+ partValues.set(part, value);
221
+ claimedParams.add(part.param);
222
+ }
223
+ }
162
224
  }
163
225
 
164
226
  if (focusedRoute === route) {
165
227
  // If this is the focused route, keep the params for later use
166
228
  // We save it here since it's been stringified already
167
- focusedParams = { ...currentParams };
168
-
169
- parts
170
- // eslint-disable-next-line no-loop-func
171
- ?.forEach(({ param }) => {
172
- if (param) {
173
- // Remove the params present in the pattern since we'll only use the rest for query string
174
- if (focusedParams) {
175
- // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
176
- delete focusedParams[param];
177
- }
178
- }
179
- });
229
+ focusedParams = {};
230
+
231
+ for (const key in currentParams) {
232
+ const value = currentParams[key];
233
+
234
+ if (
235
+ !claimedParams.has(key) &&
236
+ value !== undefined &&
237
+ value !== 'undefined'
238
+ ) {
239
+ focusedParams[key] = value;
240
+ }
241
+ }
242
+ }
243
+ }
244
+
245
+ for (const part of ownParts) {
246
+ if (part.param && !partValues.has(part)) {
247
+ partValues.set(part, undefined);
180
248
  }
181
249
  }
182
250
 
251
+ const routeState = getRouteState(route, config);
252
+
183
253
  // If there is no `screens` property or no nested state, we return pattern
184
- if (!currentOptions[route.name].screens || route.state === undefined) {
254
+ if (!config.screens || routeState === undefined) {
185
255
  hasNext = false;
186
256
  } else {
187
257
  index =
188
- typeof route.state.index === 'number'
189
- ? route.state.index
190
- : route.state.routes.length - 1;
258
+ typeof routeState.index === 'number'
259
+ ? routeState.index
260
+ : routeState.routes.length - 1;
191
261
 
192
- const nextRoute = route.state.routes[index];
193
- const nestedConfig = currentOptions[route.name].screens;
262
+ const nextRoute = routeState.routes[index];
263
+ const nestedConfig = config.screens;
194
264
 
195
265
  // if there is config for next route name, we go deeper
196
266
  if (nestedConfig && nextRoute.name in nestedConfig) {
197
- route = nextRoute as Route<string> & { state?: State };
267
+ route = nextRoute;
198
268
  currentOptions = nestedConfig;
199
269
  } else {
200
270
  // If not, there is no sense in going deeper in config
@@ -203,9 +273,12 @@ export function getPathFromState<ParamList extends {}>(
203
273
  }
204
274
  }
205
275
 
276
+ const routeState = getRouteState(route, currentOptions[route.name]);
277
+
206
278
  if (currentOptions[route.name] !== undefined) {
207
279
  path += parts
208
- ?.map(({ segment, param, optional }) => {
280
+ ?.map((part) => {
281
+ const { segment, param, optional } = part;
209
282
  // We don't know what to show for wildcard patterns
210
283
  // Showing the route name seems ok, though whatever we show here will be incorrect
211
284
  // Since the page doesn't actually exist
@@ -215,7 +288,7 @@ export function getPathFromState<ParamList extends {}>(
215
288
 
216
289
  // If the path has a pattern for a param, put the param in the path
217
290
  if (param) {
218
- const value = allParams[param];
291
+ const value = partValues.get(part);
219
292
 
220
293
  if (value === undefined && optional) {
221
294
  // Optional params without value assigned in route.params should be ignored
@@ -244,12 +317,12 @@ export function getPathFromState<ParamList extends {}>(
244
317
  focusedParams = Object.fromEntries(
245
318
  Object.entries(focusedRoute.params).map(([key, value]) => [
246
319
  key,
247
- String(value),
320
+ serializeParamValue(value),
248
321
  ])
249
322
  );
250
323
  }
251
324
 
252
- if (route.state) {
325
+ if (routeState) {
253
326
  path += '/';
254
327
  } else if (focusedParams) {
255
328
  for (const param in focusedParams) {
@@ -266,7 +339,7 @@ export function getPathFromState<ParamList extends {}>(
266
339
  }
267
340
  }
268
341
 
269
- current = route.state;
342
+ current = routeState;
270
343
  }
271
344
 
272
345
  // Include the root path if specified
@@ -292,14 +365,13 @@ const createConfigItem = (
292
365
  parentParts?: PatternPart[]
293
366
  ): ConfigItem => {
294
367
  if (typeof config === 'string') {
295
- // If a string is specified as the value of the key(e.g. Foo: '/path'), use it as the pattern
296
- const parts = getPatternParts(config);
368
+ const ownParts = getPatternParts(config);
297
369
 
298
370
  if (parentParts) {
299
- return { parts: [...parentParts, ...parts] };
371
+ return { parts: [...parentParts, ...ownParts], ownParts };
300
372
  }
301
373
 
302
- return { parts };
374
+ return { parts: ownParts, ownParts };
303
375
  }
304
376
 
305
377
  if (config.exact && config.path === undefined) {
@@ -310,14 +382,12 @@ const createConfigItem = (
310
382
 
311
383
  // If an object is specified as the value (e.g. Foo: { ... }),
312
384
  // It can have `path` property and `screens` prop which has nested configs
385
+ const ownParts = config.path ? getPatternParts(config.path) : [];
313
386
  const parts =
314
387
  config.exact !== true
315
- ? [
316
- ...(parentParts || []),
317
- ...(config.path ? getPatternParts(config.path) : []),
318
- ]
319
- : config.path
320
- ? getPatternParts(config.path)
388
+ ? [...(parentParts || []), ...ownParts]
389
+ : ownParts.length
390
+ ? ownParts
321
391
  : undefined;
322
392
 
323
393
  const screens = config.screens
@@ -326,6 +396,7 @@ const createConfigItem = (
326
396
 
327
397
  return {
328
398
  parts,
399
+ ownParts,
329
400
  stringify: config.stringify,
330
401
  screens,
331
402
  };
@@ -15,6 +15,8 @@ export function getPatternParts(path: string): PatternPart[] {
15
15
 
16
16
  let isRegex = false;
17
17
  let isParam = false;
18
+ let isEscaped = false;
19
+ let isInCharClass = false;
18
20
  let regexInnerParens = 0;
19
21
 
20
22
  // One extra iteration to add the last character
@@ -34,7 +36,7 @@ export function getPatternParts(path: string): PatternPart[] {
34
36
  `Encountered ':' in the middle of a segment in path: ${path}`
35
37
  );
36
38
  }
37
- } else if (char === '(') {
39
+ } else if (char === '(' && !isEscaped && !isInCharClass) {
38
40
  if (isParam) {
39
41
  if (isRegex) {
40
42
  // The '(' is part of the regex if we're already inside one
@@ -47,7 +49,7 @@ export function getPatternParts(path: string): PatternPart[] {
47
49
  `Encountered '(' without preceding ':' in path: ${path}`
48
50
  );
49
51
  }
50
- } else if (char === ')') {
52
+ } else if (char === ')' && !isEscaped && !isInCharClass) {
51
53
  if (isParam && isRegex) {
52
54
  if (regexInnerParens) {
53
55
  // The ')' is part of the regex if we're already inside one
@@ -102,6 +104,17 @@ export function getPatternParts(path: string): PatternPart[] {
102
104
  if (isRegex) {
103
105
  current.regex = current.regex || '';
104
106
  current.regex += char;
107
+
108
+ // Track escapes and character classes so parens inside them are not special
109
+ if (isEscaped) {
110
+ isEscaped = false;
111
+ } else if (char === '\\') {
112
+ isEscaped = true;
113
+ } else if (char === '[') {
114
+ isInCharClass = true;
115
+ } else if (char === ']') {
116
+ isInCharClass = false;
117
+ }
105
118
  }
106
119
 
107
120
  if (isParam && !isRegex) {
@@ -26,7 +26,12 @@ type RouteConfig = {
26
26
  screen: string;
27
27
  regex?: RegExp;
28
28
  segments: string[];
29
- params: { screen: string; name: string; index: number }[];
29
+ params: {
30
+ screen: string;
31
+ name: string;
32
+ index: number;
33
+ regex?: RegExp;
34
+ }[];
30
35
  routeNames: string[];
31
36
  parse?: ParseConfig;
32
37
  explicitParamNames?: Set<string>;
@@ -53,6 +58,7 @@ type ConfigResources = {
53
58
  initialRoutes: InitialRouteConfig[];
54
59
  configs: RouteConfig[];
55
60
  configsByScreen: Record<string, RouteConfig[]>;
61
+ prefixRegex?: RegExp;
56
62
  };
57
63
 
58
64
  const NESTED_SCREEN_PARAM_NAMES = [
@@ -64,6 +70,17 @@ const NESTED_SCREEN_PARAM_NAMES = [
64
70
  'pop',
65
71
  ];
66
72
 
73
+ const getStaticSegmentPattern = (segment: string) =>
74
+ Array.from(segment, (char) => {
75
+ const encoded = encodeURIComponent(char);
76
+ const percentEncoded =
77
+ encoded === char
78
+ ? `%${char.charCodeAt(0).toString(16).padStart(2, '0').toUpperCase()}`
79
+ : encoded;
80
+
81
+ return `(?:${escape(char)}|${escape(percentEncoded)})`;
82
+ }).join('');
83
+
67
84
  const getExplicitParamNames = (parse?: ParseConfig) => {
68
85
  const names = Object.entries(parse ?? {}).map(([name]) => name);
69
86
 
@@ -95,7 +112,7 @@ export function getStateFromPath<ParamList extends {}>(
95
112
  path: string,
96
113
  options?: Options<ParamList>
97
114
  ): ResultState | undefined {
98
- const { initialRoutes, configs, configsByScreen } =
115
+ const { initialRoutes, configs, configsByScreen, prefixRegex } =
99
116
  getConfigResources(options);
100
117
 
101
118
  const screens = options?.screens;
@@ -103,24 +120,20 @@ export function getStateFromPath<ParamList extends {}>(
103
120
  let remaining = path
104
121
  .replace(/\/+/g, '/') // Replace multiple slash (//) with single ones
105
122
  .replace(/^\//, '') // Remove extra leading slash
106
- .replace(/\?.*$/, ''); // Remove query params which we will handle later
123
+ .replace(/\?.*$/, '') // Remove query params which we will handle later
124
+ .replace(/%[0-9a-f]{2}/gi, (match) => match.toUpperCase());
107
125
 
108
126
  // Make sure there is a trailing slash
109
127
  remaining = remaining.endsWith('/') ? remaining : `${remaining}/`;
110
128
 
111
- const prefix = options?.path?.replace(/^\//, ''); // Remove extra leading slash
112
-
113
- if (prefix) {
114
- // Make sure there is a trailing slash
115
- const normalizedPrefix = prefix.endsWith('/') ? prefix : `${prefix}/`;
129
+ if (prefixRegex) {
130
+ const prefixMatch = remaining.match(prefixRegex);
116
131
 
117
- // If the path doesn't start with the prefix, it's not a match
118
- if (!remaining.startsWith(normalizedPrefix)) {
132
+ if (prefixMatch == null) {
119
133
  return undefined;
120
134
  }
121
135
 
122
- // Remove the prefix from the path
123
- remaining = remaining.replace(normalizedPrefix, '');
136
+ remaining = remaining.slice(prefixMatch[0].length);
124
137
  }
125
138
 
126
139
  if (screens === undefined) {
@@ -219,6 +232,19 @@ function prepareConfigResources(options?: Options<{}>) {
219
232
 
220
233
  const initialRoutes = getInitialRoutes(options);
221
234
  const configs = getSortedNormalizedConfigs(initialRoutes, options?.screens);
235
+ const prefix = options?.path?.replace(/^\//, '');
236
+
237
+ let prefixRegex: RegExp | undefined;
238
+
239
+ if (prefix) {
240
+ const normalizedPrefix = prefix.endsWith('/') ? prefix : `${prefix}/`;
241
+ const prefixPattern = normalizedPrefix
242
+ .split('/')
243
+ .map(getStaticSegmentPattern)
244
+ .join('/');
245
+
246
+ prefixRegex = new RegExp(`^${prefixPattern}`);
247
+ }
222
248
 
223
249
  const configsByScreen: Record<string, RouteConfig[]> = {};
224
250
  const configsByPattern = new Map<string, RouteConfig>();
@@ -237,6 +263,7 @@ function prepareConfigResources(options?: Options<{}>) {
237
263
  initialRoutes,
238
264
  configs,
239
265
  configsByScreen,
266
+ prefixRegex,
240
267
  };
241
268
  }
242
269
 
@@ -264,6 +291,7 @@ function getSortedNormalizedConfigs(
264
291
  createNormalizedConfigs(key, screens, initialRoutes, [], [], [])
265
292
  )
266
293
  )
294
+ .map((config, order) => ({ ...config, order }))
267
295
  .sort((a, b) => {
268
296
  // Sort config from most specific to least specific:
269
297
  // - more segments
@@ -275,7 +303,21 @@ function getSortedNormalizedConfigs(
275
303
  // If 2 patterns are same, move the one with less route names up
276
304
  // This is an error state, so it's only useful for consistent error messages
277
305
  if (isArrayEqual(a.segments, b.segments)) {
278
- return b.routeNames.join('>').localeCompare(a.routeNames.join('>'));
306
+ if (
307
+ a.routeNames.length > b.routeNames.length &&
308
+ arrayStartsWith(a.routeNames, b.routeNames)
309
+ ) {
310
+ return -1;
311
+ }
312
+
313
+ if (
314
+ b.routeNames.length > a.routeNames.length &&
315
+ arrayStartsWith(b.routeNames, a.routeNames)
316
+ ) {
317
+ return 1;
318
+ }
319
+
320
+ return a.routeNames.length - b.routeNames.length || a.order - b.order;
279
321
  }
280
322
 
281
323
  // If one of the patterns starts with the other, it's more exhaustive
@@ -441,6 +483,15 @@ const matchAgainstConfigs = (
441
483
  break;
442
484
  }
443
485
 
486
+ if (
487
+ param.regex &&
488
+ value !== decoded &&
489
+ !param.regex.test(decoded)
490
+ ) {
491
+ hasInvalidParam = true;
492
+ break;
493
+ }
494
+
444
495
  const parser = routeConfig.parse?.[param.name];
445
496
 
446
497
  paramEntries.push([param.name, parser ? parser(decoded) : decoded]);
@@ -642,12 +693,18 @@ const createConfigItem = (
642
693
  `^(${parts
643
694
  .map((it, i) => {
644
695
  if (it.param) {
645
- const reg = it.regex || '[^/]+';
696
+ const reg = it.regex
697
+ ? `(?:${it.regex})|(?=[^/]*%[0-9A-F]{2})[^/]+`
698
+ : '[^/]+';
646
699
 
647
700
  return `(((?<param_${i}>${reg})\\/)${it.optional ? '?' : ''})`;
648
701
  }
649
702
 
650
- return `${it.segment === '*' ? '.*' : escape(it.segment)}\\/`;
703
+ if (it.segment === '*') {
704
+ return `.*\\/`;
705
+ }
706
+
707
+ return `${getStaticSegmentPattern(it.segment)}\\/`;
651
708
  })
652
709
  .join('')})$`
653
710
  )
@@ -666,6 +723,7 @@ const createConfigItem = (
666
723
  index,
667
724
  screen: part.screen,
668
725
  name: part.param,
726
+ regex: part.regex ? new RegExp(`^(?:${part.regex})$`) : undefined,
669
727
  });
670
728
 
671
729
  if (part.screen === screen) {
@@ -813,8 +871,9 @@ const parseQueryParams = (
813
871
  hasNestedScreens = false,
814
872
  routeParams?: Record<string, unknown>
815
873
  ) => {
816
- const query = path.split('?')[1];
817
- const params: Record<string, unknown> = queryString.parse(query);
874
+ const queryIndex = path.indexOf('?');
875
+ const query = queryIndex === -1 ? undefined : path.slice(queryIndex + 1);
876
+ const params: Record<string, unknown> = query ? queryString.parse(query) : {};
818
877
 
819
878
  for (const name of pathParamNames) {
820
879
  // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
@@ -0,0 +1,60 @@
1
+ import type { NavigationState, PartialState } from '@react-navigation/routers';
2
+
3
+ export function getStateFromRouteParams(
4
+ params: object | undefined
5
+ ): PartialState<NavigationState> | NavigationState | undefined {
6
+ if (params == null || typeof params !== 'object') {
7
+ return undefined;
8
+ }
9
+
10
+ if (
11
+ 'state' in params &&
12
+ params.state &&
13
+ typeof params.state === 'object' &&
14
+ 'routes' in params.state &&
15
+ Array.isArray(params.state.routes) &&
16
+ params.state.routes.every(
17
+ (route) =>
18
+ typeof route === 'object' &&
19
+ route != null &&
20
+ 'name' in route &&
21
+ typeof route.name === 'string'
22
+ )
23
+ ) {
24
+ // @ts-expect-error this is fine 🔥
25
+ return params.state;
26
+ }
27
+
28
+ if (
29
+ 'screen' in params &&
30
+ params.screen &&
31
+ typeof params.screen === 'string'
32
+ ) {
33
+ return {
34
+ routes: [
35
+ {
36
+ name: params.screen,
37
+ params:
38
+ 'params' in params &&
39
+ typeof params.params === 'object' &&
40
+ params.params != null
41
+ ? params.params
42
+ : undefined,
43
+ path:
44
+ 'path' in params && typeof params.path === 'string'
45
+ ? params.path
46
+ : undefined,
47
+ // @ts-expect-error this is fine 🔥
48
+ state:
49
+ 'params' in params &&
50
+ typeof params.params === 'object' &&
51
+ params.params != null
52
+ ? getStateFromRouteParams(params.params)
53
+ : undefined,
54
+ },
55
+ ],
56
+ };
57
+ }
58
+
59
+ return undefined;
60
+ }