@react-navigation/core 8.0.0-alpha.2 → 8.0.0-alpha.21

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 (251) hide show
  1. package/lib/module/BaseNavigationContainer.js +20 -9
  2. package/lib/module/BaseNavigationContainer.js.map +1 -1
  3. package/lib/module/ConsumedParamsContext.js +5 -0
  4. package/lib/module/ConsumedParamsContext.js.map +1 -0
  5. package/lib/module/NavigationBuilderContext.js +1 -0
  6. package/lib/module/NavigationBuilderContext.js.map +1 -1
  7. package/lib/module/NavigationIndependentTree.js +8 -4
  8. package/lib/module/NavigationIndependentTree.js.map +1 -1
  9. package/lib/module/NavigationProvider.js +14 -3
  10. package/lib/module/NavigationProvider.js.map +1 -1
  11. package/lib/module/NavigationStateContext.js.map +1 -1
  12. package/lib/module/PreventRemoveProvider.js +3 -3
  13. package/lib/module/PreventRemoveProvider.js.map +1 -1
  14. package/lib/module/SceneView.js +46 -56
  15. package/lib/module/SceneView.js.map +1 -1
  16. package/lib/module/StaticNavigation.js +166 -46
  17. package/lib/module/StaticNavigation.js.map +1 -1
  18. package/lib/module/checkSerializable.js +11 -4
  19. package/lib/module/checkSerializable.js.map +1 -1
  20. package/lib/module/createNavigatorFactory.js +27 -5
  21. package/lib/module/createNavigatorFactory.js.map +1 -1
  22. package/lib/module/findFocusedRoute.js +7 -4
  23. package/lib/module/findFocusedRoute.js.map +1 -1
  24. package/lib/module/getActionFromState.js +40 -14
  25. package/lib/module/getActionFromState.js.map +1 -1
  26. package/lib/module/getPathFromState.js +48 -11
  27. package/lib/module/getPathFromState.js.map +1 -1
  28. package/lib/module/getPatternParts.js +3 -0
  29. package/lib/module/getPatternParts.js.map +1 -1
  30. package/lib/module/getStateFromPath.js +353 -166
  31. package/lib/module/getStateFromPath.js.map +1 -1
  32. package/lib/module/getStateFromRouteParams.js +24 -0
  33. package/lib/module/getStateFromRouteParams.js.map +1 -0
  34. package/lib/module/index.js +3 -4
  35. package/lib/module/index.js.map +1 -1
  36. package/lib/module/theming/useTheme.js +1 -1
  37. package/lib/module/theming/useTheme.js.map +1 -1
  38. package/lib/module/types.js +15 -0
  39. package/lib/module/types.js.map +1 -1
  40. package/lib/module/useDescriptors.js +18 -9
  41. package/lib/module/useDescriptors.js.map +1 -1
  42. package/lib/module/useEventEmitter.js +43 -31
  43. package/lib/module/useEventEmitter.js.map +1 -1
  44. package/lib/module/useFocusEvents.js +6 -2
  45. package/lib/module/useFocusEvents.js.map +1 -1
  46. package/lib/module/useFocusedListenersChildrenAdapter.js +2 -2
  47. package/lib/module/useFocusedListenersChildrenAdapter.js.map +1 -1
  48. package/lib/module/useIsFocused.js +7 -12
  49. package/lib/module/useIsFocused.js.map +1 -1
  50. package/lib/module/useNavigation.js.map +1 -1
  51. package/lib/module/useNavigationBuilder.js +144 -86
  52. package/lib/module/useNavigationBuilder.js.map +1 -1
  53. package/lib/module/useNavigationCache.js +3 -3
  54. package/lib/module/useNavigationCache.js.map +1 -1
  55. package/lib/module/useNavigationHelpers.js +1 -1
  56. package/lib/module/useNavigationHelpers.js.map +1 -1
  57. package/lib/module/useNavigationIndependentTree.js +1 -1
  58. package/lib/module/useNavigationIndependentTree.js.map +1 -1
  59. package/lib/module/useNavigationState.js +6 -4
  60. package/lib/module/useNavigationState.js.map +1 -1
  61. package/lib/module/useOnAction.js +3 -3
  62. package/lib/module/useOnAction.js.map +1 -1
  63. package/lib/module/useOnGetState.js +6 -3
  64. package/lib/module/useOnGetState.js.map +1 -1
  65. package/lib/module/useOnPreventRemove.js +3 -3
  66. package/lib/module/useOnPreventRemove.js.map +1 -1
  67. package/lib/module/useOnRouteFocus.js +1 -1
  68. package/lib/module/useOnRouteFocus.js.map +1 -1
  69. package/lib/module/useOptionsGetters.js +7 -4
  70. package/lib/module/useOptionsGetters.js.map +1 -1
  71. package/lib/module/usePreventRemove.js +1 -1
  72. package/lib/module/usePreventRemove.js.map +1 -1
  73. package/lib/module/usePreventRemoveContext.js +1 -1
  74. package/lib/module/usePreventRemoveContext.js.map +1 -1
  75. package/lib/module/useRegisterNavigator.js +2 -2
  76. package/lib/module/useRegisterNavigator.js.map +1 -1
  77. package/lib/module/useRoute.js.map +1 -1
  78. package/lib/module/useRouteCache.js +2 -1
  79. package/lib/module/useRouteCache.js.map +1 -1
  80. package/lib/module/useScheduleUpdate.js +1 -1
  81. package/lib/module/useScheduleUpdate.js.map +1 -1
  82. package/lib/module/useStateForPath.js +1 -1
  83. package/lib/module/useStateForPath.js.map +1 -1
  84. package/lib/module/validatePathConfig.js +2 -1
  85. package/lib/module/validatePathConfig.js.map +1 -1
  86. package/lib/typescript/src/BaseNavigationContainer.d.ts +6 -3
  87. package/lib/typescript/src/BaseNavigationContainer.d.ts.map +1 -1
  88. package/lib/typescript/src/ConsumedParamsContext.d.ts +8 -0
  89. package/lib/typescript/src/ConsumedParamsContext.d.ts.map +1 -0
  90. package/lib/typescript/src/EnsureSingleNavigator.d.ts +1 -1
  91. package/lib/typescript/src/EnsureSingleNavigator.d.ts.map +1 -1
  92. package/lib/typescript/src/Group.d.ts +1 -1
  93. package/lib/typescript/src/Group.d.ts.map +1 -1
  94. package/lib/typescript/src/NavigationBuilderContext.d.ts +12 -6
  95. package/lib/typescript/src/NavigationBuilderContext.d.ts.map +1 -1
  96. package/lib/typescript/src/NavigationContainerRefContext.d.ts +1 -1
  97. package/lib/typescript/src/NavigationContainerRefContext.d.ts.map +1 -1
  98. package/lib/typescript/src/NavigationFocusedRouteStateContext.d.ts +4 -4
  99. package/lib/typescript/src/NavigationFocusedRouteStateContext.d.ts.map +1 -1
  100. package/lib/typescript/src/NavigationHelpersContext.d.ts +1 -1
  101. package/lib/typescript/src/NavigationHelpersContext.d.ts.map +1 -1
  102. package/lib/typescript/src/NavigationIndependentTree.d.ts +1 -1
  103. package/lib/typescript/src/NavigationIndependentTree.d.ts.map +1 -1
  104. package/lib/typescript/src/NavigationProvider.d.ts +10 -10
  105. package/lib/typescript/src/NavigationProvider.d.ts.map +1 -1
  106. package/lib/typescript/src/NavigationStateContext.d.ts +3 -3
  107. package/lib/typescript/src/NavigationStateContext.d.ts.map +1 -1
  108. package/lib/typescript/src/PreventRemoveProvider.d.ts +1 -1
  109. package/lib/typescript/src/PreventRemoveProvider.d.ts.map +1 -1
  110. package/lib/typescript/src/SceneView.d.ts +3 -2
  111. package/lib/typescript/src/SceneView.d.ts.map +1 -1
  112. package/lib/typescript/src/Screen.d.ts +1 -1
  113. package/lib/typescript/src/Screen.d.ts.map +1 -1
  114. package/lib/typescript/src/StaticContainer.d.ts +1 -1
  115. package/lib/typescript/src/StaticContainer.d.ts.map +1 -1
  116. package/lib/typescript/src/StaticNavigation.d.ts +123 -92
  117. package/lib/typescript/src/StaticNavigation.d.ts.map +1 -1
  118. package/lib/typescript/src/checkSerializable.d.ts +5 -3
  119. package/lib/typescript/src/checkSerializable.d.ts.map +1 -1
  120. package/lib/typescript/src/createNavigationContainerRef.d.ts +1 -1
  121. package/lib/typescript/src/createNavigationContainerRef.d.ts.map +1 -1
  122. package/lib/typescript/src/createNavigatorFactory.d.ts +17 -5
  123. package/lib/typescript/src/createNavigatorFactory.d.ts.map +1 -1
  124. package/lib/typescript/src/findFocusedRoute.d.ts +3 -3
  125. package/lib/typescript/src/findFocusedRoute.d.ts.map +1 -1
  126. package/lib/typescript/src/getActionFromState.d.ts +4 -4
  127. package/lib/typescript/src/getActionFromState.d.ts.map +1 -1
  128. package/lib/typescript/src/getPathFromState.d.ts +3 -3
  129. package/lib/typescript/src/getPathFromState.d.ts.map +1 -1
  130. package/lib/typescript/src/getPatternParts.d.ts +1 -0
  131. package/lib/typescript/src/getPatternParts.d.ts.map +1 -1
  132. package/lib/typescript/src/getStateFromPath.d.ts +5 -5
  133. package/lib/typescript/src/getStateFromPath.d.ts.map +1 -1
  134. package/lib/typescript/src/getStateFromRouteParams.d.ts +3 -0
  135. package/lib/typescript/src/getStateFromRouteParams.d.ts.map +1 -0
  136. package/lib/typescript/src/index.d.ts +34 -33
  137. package/lib/typescript/src/index.d.ts.map +1 -1
  138. package/lib/typescript/src/theming/ThemeContext.d.ts +1 -1
  139. package/lib/typescript/src/theming/ThemeContext.d.ts.map +1 -1
  140. package/lib/typescript/src/theming/ThemeProvider.d.ts +2 -2
  141. package/lib/typescript/src/theming/ThemeProvider.d.ts.map +1 -1
  142. package/lib/typescript/src/theming/useTheme.d.ts +1 -1
  143. package/lib/typescript/src/theming/useTheme.d.ts.map +1 -1
  144. package/lib/typescript/src/types.d.ts +215 -131
  145. package/lib/typescript/src/types.d.ts.map +1 -1
  146. package/lib/typescript/src/useChildListeners.d.ts +3 -3
  147. package/lib/typescript/src/useChildListeners.d.ts.map +1 -1
  148. package/lib/typescript/src/useComponent.d.ts +1 -1
  149. package/lib/typescript/src/useComponent.d.ts.map +1 -1
  150. package/lib/typescript/src/useDescriptors.d.ts +7 -7
  151. package/lib/typescript/src/useDescriptors.d.ts.map +1 -1
  152. package/lib/typescript/src/useEventEmitter.d.ts +1 -1
  153. package/lib/typescript/src/useEventEmitter.d.ts.map +1 -1
  154. package/lib/typescript/src/useFocusEvents.d.ts +2 -2
  155. package/lib/typescript/src/useFocusEvents.d.ts.map +1 -1
  156. package/lib/typescript/src/useFocusedListenersChildrenAdapter.d.ts +2 -2
  157. package/lib/typescript/src/useFocusedListenersChildrenAdapter.d.ts.map +1 -1
  158. package/lib/typescript/src/useIsFocused.d.ts +3 -0
  159. package/lib/typescript/src/useIsFocused.d.ts.map +1 -1
  160. package/lib/typescript/src/useKeyedChildListeners.d.ts +3 -3
  161. package/lib/typescript/src/useKeyedChildListeners.d.ts.map +1 -1
  162. package/lib/typescript/src/useNavigation.d.ts +4 -2
  163. package/lib/typescript/src/useNavigation.d.ts.map +1 -1
  164. package/lib/typescript/src/useNavigationBuilder.d.ts +24 -24
  165. package/lib/typescript/src/useNavigationBuilder.d.ts.map +1 -1
  166. package/lib/typescript/src/useNavigationCache.d.ts +3 -3
  167. package/lib/typescript/src/useNavigationCache.d.ts.map +1 -1
  168. package/lib/typescript/src/useNavigationContainerRef.d.ts +1 -1
  169. package/lib/typescript/src/useNavigationContainerRef.d.ts.map +1 -1
  170. package/lib/typescript/src/useNavigationHelpers.d.ts +19 -19
  171. package/lib/typescript/src/useNavigationHelpers.d.ts.map +1 -1
  172. package/lib/typescript/src/useNavigationState.d.ts +2 -2
  173. package/lib/typescript/src/useNavigationState.d.ts.map +1 -1
  174. package/lib/typescript/src/useOnAction.d.ts +9 -9
  175. package/lib/typescript/src/useOnAction.d.ts.map +1 -1
  176. package/lib/typescript/src/useOnGetState.d.ts +1 -1
  177. package/lib/typescript/src/useOnGetState.d.ts.map +1 -1
  178. package/lib/typescript/src/useOnPreventRemove.d.ts +3 -3
  179. package/lib/typescript/src/useOnPreventRemove.d.ts.map +1 -1
  180. package/lib/typescript/src/useOnRouteFocus.d.ts +6 -6
  181. package/lib/typescript/src/useOnRouteFocus.d.ts.map +1 -1
  182. package/lib/typescript/src/useOptionsGetters.d.ts +1 -1
  183. package/lib/typescript/src/useOptionsGetters.d.ts.map +1 -1
  184. package/lib/typescript/src/usePreventRemoveContext.d.ts +1 -1
  185. package/lib/typescript/src/useRoute.d.ts +2 -2
  186. package/lib/typescript/src/useRoute.d.ts.map +1 -1
  187. package/lib/typescript/src/useRouteCache.d.ts +4 -4
  188. package/lib/typescript/src/useRouteCache.d.ts.map +1 -1
  189. package/lib/typescript/src/useScheduleUpdate.d.ts.map +1 -1
  190. package/lib/typescript/src/useStateForPath.d.ts +1 -1
  191. package/lib/typescript/src/useStateForPath.d.ts.map +1 -1
  192. package/lib/typescript/src/utilities.d.ts +45 -5
  193. package/lib/typescript/src/utilities.d.ts.map +1 -1
  194. package/lib/typescript/src/validatePathConfig.d.ts.map +1 -1
  195. package/package.json +23 -20
  196. package/src/BaseNavigationContainer.tsx +337 -326
  197. package/src/ConsumedParamsContext.tsx +10 -0
  198. package/src/NavigationBuilderContext.tsx +14 -8
  199. package/src/NavigationFocusedRouteStateContext.tsx +4 -4
  200. package/src/NavigationIndependentTree.tsx +8 -5
  201. package/src/NavigationProvider.tsx +17 -3
  202. package/src/NavigationStateContext.tsx +5 -6
  203. package/src/PreventRemoveProvider.tsx +3 -3
  204. package/src/SceneView.tsx +58 -56
  205. package/src/StaticNavigation.tsx +488 -250
  206. package/src/checkSerializable.tsx +22 -12
  207. package/src/createNavigatorFactory.tsx +83 -8
  208. package/src/findFocusedRoute.tsx +13 -8
  209. package/src/getActionFromState.tsx +68 -39
  210. package/src/getPathFromState.tsx +91 -27
  211. package/src/getPatternParts.tsx +8 -0
  212. package/src/getStateFromPath.tsx +555 -180
  213. package/src/getStateFromRouteParams.tsx +60 -0
  214. package/src/index.tsx +47 -5
  215. package/src/theming/useTheme.tsx +1 -1
  216. package/src/types.tsx +541 -338
  217. package/src/useDescriptors.tsx +27 -12
  218. package/src/useEventEmitter.tsx +45 -36
  219. package/src/useFocusEvents.tsx +8 -2
  220. package/src/useFocusedListenersChildrenAdapter.tsx +2 -2
  221. package/src/useIsFocused.tsx +14 -21
  222. package/src/useNavigation.tsx +5 -3
  223. package/src/useNavigationBuilder.tsx +220 -132
  224. package/src/useNavigationCache.tsx +6 -6
  225. package/src/useNavigationHelpers.tsx +1 -1
  226. package/src/useNavigationIndependentTree.tsx +1 -1
  227. package/src/useNavigationState.tsx +11 -7
  228. package/src/useOnAction.tsx +10 -10
  229. package/src/useOnGetState.tsx +5 -3
  230. package/src/useOnPreventRemove.tsx +3 -3
  231. package/src/useOnRouteFocus.tsx +10 -12
  232. package/src/useOptionsGetters.tsx +7 -4
  233. package/src/usePreventRemove.tsx +1 -1
  234. package/src/usePreventRemoveContext.tsx +1 -1
  235. package/src/useRegisterNavigator.tsx +2 -2
  236. package/src/useRoute.tsx +4 -2
  237. package/src/useRouteCache.tsx +2 -1
  238. package/src/useScheduleUpdate.tsx +1 -3
  239. package/src/useStateForPath.tsx +1 -1
  240. package/src/utilities.tsx +101 -5
  241. package/src/validatePathConfig.tsx +1 -0
  242. package/lib/module/CurrentRenderContext.js +0 -10
  243. package/lib/module/CurrentRenderContext.js.map +0 -1
  244. package/lib/module/useCurrentRender.js +0 -19
  245. package/lib/module/useCurrentRender.js.map +0 -1
  246. package/lib/typescript/src/CurrentRenderContext.d.ts +0 -9
  247. package/lib/typescript/src/CurrentRenderContext.d.ts.map +0 -1
  248. package/lib/typescript/src/useCurrentRender.d.ts +0 -14
  249. package/lib/typescript/src/useCurrentRender.d.ts.map +0 -1
  250. package/src/CurrentRenderContext.tsx +0 -9
  251. package/src/useCurrentRender.tsx +0 -31
@@ -4,30 +4,46 @@ import type {
4
4
  PartialState,
5
5
  } from '@react-navigation/routers';
6
6
  import escape from 'escape-string-regexp';
7
- import * as queryString from 'query-string';
7
+ import queryString from 'query-string';
8
8
 
9
9
  import { arrayStartsWith } from './arrayStartsWith';
10
10
  import { findFocusedRoute } from './findFocusedRoute';
11
- import { getPatternParts, type PatternPart } from './getPatternParts';
11
+ import {
12
+ combinePatternParts,
13
+ getPatternParts,
14
+ type PatternPart,
15
+ } from './getPatternParts';
12
16
  import { isArrayEqual } from './isArrayEqual';
13
17
  import type { PathConfig, PathConfigMap } from './types';
18
+ import type {
19
+ StandardSchemaV1,
20
+ StandardSchemaValidationResult,
21
+ } from './utilities';
14
22
  import { validatePathConfig } from './validatePathConfig';
15
23
 
16
24
  type Options<ParamList extends {}> = {
17
- path?: string;
18
- initialRouteName?: string;
25
+ path?: string | undefined;
26
+ initialRouteName?: string | undefined;
19
27
  screens: PathConfigMap<ParamList>;
20
28
  };
21
29
 
22
- type ParseConfig = Record<string, ((value: string) => unknown) | undefined>;
30
+ type ParseConfigValue =
31
+ | ((value: string) => unknown)
32
+ | StandardSchemaV1<unknown, unknown>;
33
+
34
+ type ParseConfig = Record<string, ParseConfigValue | undefined>;
23
35
 
24
36
  type RouteConfig = {
25
37
  screen: string;
26
- regex?: RegExp;
38
+ order: number;
39
+ regex?: RegExp | undefined;
27
40
  segments: string[];
28
- params: { screen: string; name?: string; index: number }[];
41
+ params: { screen: string; name?: string | undefined; index: number }[];
29
42
  routeNames: string[];
30
- parse?: ParseConfig;
43
+ parse?: ParseConfig | undefined;
44
+ explicitParamNames?: Set<string> | undefined;
45
+ hasNestedScreens: boolean;
46
+ shared: boolean;
31
47
  };
32
48
 
33
49
  type InitialRouteConfig = {
@@ -36,18 +52,80 @@ type InitialRouteConfig = {
36
52
  };
37
53
 
38
54
  type ResultState = PartialState<NavigationState> & {
39
- state?: ResultState;
55
+ state?: ResultState | undefined;
40
56
  };
41
57
 
42
58
  type ParsedRoute = {
43
59
  name: string;
44
- path?: string;
60
+ path?: string | undefined;
45
61
  params?: Record<string, unknown> | undefined;
46
62
  };
47
63
 
64
+ type RoutePatternPart = PatternPart & { screen: string };
65
+
48
66
  type ConfigResources = {
49
67
  initialRoutes: InitialRouteConfig[];
50
68
  configs: RouteConfig[];
69
+ configsByScreen: Record<string, RouteConfig[]>;
70
+ configsByPattern: Record<string, RouteConfig[]>;
71
+ };
72
+
73
+ const INVALID_SCHEMA_RESULT_ERROR =
74
+ 'Invalid validation result from schema. It should be an object with either "value" or "issues" property and cannot be asynchronous.';
75
+
76
+ const INVALID_PARSER_ERROR =
77
+ 'Invalid parser. Expected a function or a Standard Schema V1 object.';
78
+
79
+ const PARAM_GROUP_PREFIX = 'param_';
80
+
81
+ const NESTED_SCREEN_PARAM_NAMES = [
82
+ 'screen',
83
+ 'params',
84
+ 'initial',
85
+ 'path',
86
+ 'merge',
87
+ 'pop',
88
+ ];
89
+
90
+ const getExplicitParamNames = (parse?: ParseConfig) => {
91
+ const names = Object.entries(parse ?? {})
92
+ .filter(([, parser]) => parser != null)
93
+ .map(([name]) => name);
94
+
95
+ return names.length ? new Set(names) : undefined;
96
+ };
97
+
98
+ const getStandardSchema = (parser: ParseConfigValue) => {
99
+ if (
100
+ '~standard' in parser &&
101
+ typeof parser['~standard'] === 'object' &&
102
+ parser['~standard'] !== null &&
103
+ 'version' in parser['~standard'] &&
104
+ parser['~standard'].version === 1 &&
105
+ 'validate' in parser['~standard'] &&
106
+ typeof parser['~standard'].validate === 'function'
107
+ ) {
108
+ return parser['~standard'];
109
+ }
110
+
111
+ return undefined;
112
+ };
113
+
114
+ const getValidationResult = (
115
+ schema: StandardSchemaV1<unknown, unknown>['~standard'],
116
+ value: unknown
117
+ ): StandardSchemaValidationResult<unknown> => {
118
+ const result = schema.validate(value);
119
+
120
+ if (
121
+ result != null &&
122
+ typeof result === 'object' &&
123
+ ('value' in result || ('issues' in result && Array.isArray(result.issues)))
124
+ ) {
125
+ return result;
126
+ }
127
+
128
+ throw new Error(INVALID_SCHEMA_RESULT_ERROR);
51
129
  };
52
130
 
53
131
  /**
@@ -73,12 +151,28 @@ type ConfigResources = {
73
151
  */
74
152
  export function getStateFromPath<ParamList extends {}>(
75
153
  path: string,
76
- options?: Options<ParamList>
154
+ options?: Options<ParamList>,
155
+ previous?: NavigationState
77
156
  ): ResultState | undefined {
78
- const { initialRoutes, configs } = getConfigResources(options);
157
+ const { initialRoutes, configs, configsByScreen, configsByPattern } =
158
+ getConfigResources(options);
79
159
 
80
160
  const screens = options?.screens;
81
161
 
162
+ // When a matched config is shared, the same pattern can resolve to multiple branches
163
+ // pick the one that best matches the current navigation state
164
+ const resolveSharedConfig = (config: RouteConfig) => {
165
+ if (config.shared) {
166
+ const candidates = configsByPattern[config.segments.join('/')];
167
+
168
+ if (candidates) {
169
+ return selectSharedConfig(config, candidates, previous, initialRoutes);
170
+ }
171
+ }
172
+
173
+ return config;
174
+ };
175
+
82
176
  let remaining = path
83
177
  .replace(/\/+/g, '/') // Replace multiple slash (//) with single ones
84
178
  .replace(/^\//, '') // Remove extra leading slash
@@ -102,15 +196,23 @@ export function getStateFromPath<ParamList extends {}>(
102
196
  remaining = remaining.replace(normalizedPrefix, '');
103
197
  }
104
198
 
199
+ const decodedSegments: string[] = [];
200
+
201
+ for (const segment of remaining.split('/')) {
202
+ if (!segment) {
203
+ continue;
204
+ }
205
+
206
+ try {
207
+ decodedSegments.push(decodeURIComponent(segment));
208
+ } catch {
209
+ return undefined;
210
+ }
211
+ }
212
+
105
213
  if (screens === undefined) {
106
214
  // When no config is specified, use the path segments as route names
107
- const routes = remaining
108
- .split('/')
109
- .filter(Boolean)
110
- .map((segment) => {
111
- const name = decodeURIComponent(segment);
112
- return { name };
113
- });
215
+ const routes = decodedSegments.map((name) => ({ name }));
114
216
 
115
217
  if (routes.length) {
116
218
  return createNestedStateObject(path, routes, initialRoutes);
@@ -125,36 +227,58 @@ export function getStateFromPath<ParamList extends {}>(
125
227
  const match = configs.find((config) => config.segments.join('/') === '');
126
228
 
127
229
  if (match) {
230
+ const config = resolveSharedConfig(match);
231
+
128
232
  return createNestedStateObject(
129
233
  path,
130
- match.routeNames.map((name) => ({ name })),
234
+ config.routeNames.map((name) => ({ name })),
131
235
  initialRoutes,
132
- configs
236
+ config
133
237
  );
134
238
  }
135
239
 
136
240
  return undefined;
137
241
  }
138
242
 
139
- let result: PartialState<NavigationState> | undefined;
140
- let current: PartialState<NavigationState> | undefined;
141
-
142
243
  // We match the whole path against the regex instead of segments
143
244
  // This makes sure matches such as wildcard will catch any unmatched routes, even if nested
144
- const { routes, remainingPath } = matchAgainstConfigs(remaining, configs);
245
+ for (const config of configs) {
246
+ const routes = matchAgainstConfig(remaining, config, configsByScreen);
145
247
 
146
- if (routes !== undefined) {
147
- // This will always be empty if full path matched
148
- current = createNestedStateObject(path, routes, initialRoutes, configs);
149
- remaining = remainingPath;
150
- result = current;
151
- }
248
+ if (routes === undefined) {
249
+ continue;
250
+ }
152
251
 
153
- if (current == null || result == null) {
154
- return undefined;
252
+ let selectedConfig = resolveSharedConfig(config);
253
+ let selectedRoutes = routes;
254
+
255
+ if (selectedConfig !== config) {
256
+ const rematchedRoutes = matchAgainstConfig(
257
+ remaining,
258
+ selectedConfig,
259
+ configsByScreen
260
+ );
261
+
262
+ if (rematchedRoutes === undefined) {
263
+ selectedConfig = config;
264
+ } else {
265
+ selectedRoutes = rematchedRoutes;
266
+ }
267
+ }
268
+
269
+ const state = createNestedStateObject(
270
+ path,
271
+ selectedRoutes,
272
+ initialRoutes,
273
+ selectedConfig
274
+ );
275
+
276
+ if (state !== undefined) {
277
+ return state;
278
+ }
155
279
  }
156
280
 
157
- return result;
281
+ return undefined;
158
282
  }
159
283
 
160
284
  /**
@@ -188,12 +312,19 @@ function prepareConfigResources(options?: Options<{}>) {
188
312
 
189
313
  checkForDuplicatedConfigs(configs);
190
314
 
191
- const configWithRegexes = getConfigsWithRegexes(configs);
315
+ const configsByScreen: Record<string, RouteConfig[]> = {};
316
+ const configsByPattern: Record<string, RouteConfig[]> = {};
317
+
318
+ for (const c of configs) {
319
+ (configsByScreen[c.screen] ??= []).push(c);
320
+ (configsByPattern[c.segments.join('/')] ??= []).push(c);
321
+ }
192
322
 
193
323
  return {
194
324
  initialRoutes,
195
325
  configs,
196
- configWithRegexes,
326
+ configsByScreen,
327
+ configsByPattern,
197
328
  };
198
329
  }
199
330
 
@@ -221,6 +352,7 @@ function getSortedNormalizedConfigs(
221
352
  createNormalizedConfigs(key, screens, initialRoutes, [], [], [])
222
353
  )
223
354
  )
355
+ .map((config, order) => ({ ...config, order }))
224
356
  .sort((a, b) => {
225
357
  // Sort config from most specific to least specific:
226
358
  // - more segments
@@ -246,22 +378,25 @@ function getSortedNormalizedConfigs(
246
378
  }
247
379
 
248
380
  for (let i = 0; i < Math.max(a.segments.length, b.segments.length); i++) {
381
+ const aSegment = a.segments[i];
382
+ const bSegment = b.segments[i];
383
+
249
384
  // if b is longer, b gets higher priority
250
- if (a.segments[i] == null) {
385
+ if (aSegment == null) {
251
386
  return 1;
252
387
  }
253
388
 
254
389
  // if a is longer, a gets higher priority
255
- if (b.segments[i] == null) {
390
+ if (bSegment == null) {
256
391
  return -1;
257
392
  }
258
393
 
259
- const aWildCard = a.segments[i] === '*';
260
- const bWildCard = b.segments[i] === '*';
261
- const aParam = a.segments[i].startsWith(':');
262
- const bParam = b.segments[i].startsWith(':');
263
- const aRegex = aParam && a.segments[i].includes('(');
264
- const bRegex = bParam && b.segments[i].includes('(');
394
+ const aWildCard = aSegment === '*';
395
+ const bWildCard = bSegment === '*';
396
+ const aParam = aSegment.startsWith(':');
397
+ const bParam = bSegment.startsWith(':');
398
+ const aRegex = aParam && aSegment.includes('(');
399
+ const bRegex = bParam && bSegment.includes('(');
265
400
 
266
401
  // if both are wildcard or regex, we compare next component
267
402
  if ((aWildCard && bWildCard) || (aRegex && bRegex)) {
@@ -319,13 +454,13 @@ function checkForDuplicatedConfigs(configs: RouteConfig[]) {
319
454
  ? b.every((it, i) => a[i] === it)
320
455
  : a.every((it, i) => b[i] === it);
321
456
 
322
- if (!intersects) {
457
+ if (!intersects && (!acc[pattern].shared || !config.shared)) {
323
458
  throw new Error(
324
459
  `Found conflicting screens with the same pattern. The pattern '${
325
460
  pattern
326
461
  }' resolves to both '${a.join(' > ')}' and '${b.join(
327
462
  ' > '
328
- )}'. Patterns must be unique and cannot resolve to more than one screen.`
463
+ )}'. Patterns must be unique and cannot resolve to more than one screen unless shared: true is specified.`
329
464
  );
330
465
  }
331
466
  }
@@ -336,90 +471,118 @@ function checkForDuplicatedConfigs(configs: RouteConfig[]) {
336
471
  }, {});
337
472
  }
338
473
 
339
- function getConfigsWithRegexes(configs: RouteConfig[]) {
340
- return configs.map((c) => ({
341
- ...c,
342
- // Add `$` to the regex to make sure it matches till end of the path and not just beginning
343
- regex: c.regex ? new RegExp(c.regex.source + '$') : undefined,
344
- }));
345
- }
474
+ const matchAgainstConfig = (
475
+ remaining: string,
476
+ config: RouteConfig,
477
+ configsByScreen: Record<string, RouteConfig[]>
478
+ ) => {
479
+ if (!config.regex) {
480
+ return undefined;
481
+ }
346
482
 
347
- const matchAgainstConfigs = (remaining: string, configs: RouteConfig[]) => {
348
- let routes: ParsedRoute[] | undefined;
349
- let remainingPath = remaining;
483
+ const match = remaining.match(config.regex);
350
484
 
351
- // Go through all configs, and see if the next path segment matches our regex
352
- for (const config of configs) {
353
- if (!config.regex) {
354
- continue;
355
- }
485
+ if (!match) {
486
+ return undefined;
487
+ }
356
488
 
357
- const match = remainingPath.match(config.regex);
489
+ let validationFailed = false;
490
+ const matchedRoutes: ParsedRoute[] = [];
358
491
 
359
- // If our regex matches, we need to extract params from the path
360
- if (match) {
361
- routes = config.routeNames.map((routeName) => {
362
- const routeConfig = configs.find((c) => {
363
- // Check matching name AND pattern in case same screen is used at different levels in config
364
- return (
365
- c.screen === routeName &&
366
- arrayStartsWith(config.segments, c.segments)
367
- );
368
- });
492
+ for (const [index, routeName] of config.routeNames.entries()) {
493
+ // Check matching name, route names and pattern in case same screen is used
494
+ // at different levels or branches in config
495
+ const routeConfig = configsByScreen[routeName]?.find(
496
+ (c) =>
497
+ index === c.routeNames.length - 1 &&
498
+ arrayStartsWith(config.routeNames, c.routeNames) &&
499
+ arrayStartsWith(config.segments, c.segments)
500
+ );
369
501
 
370
- const params =
371
- routeConfig && match.groups
372
- ? Object.fromEntries(
373
- Object.entries(match.groups)
374
- .map(([key, value]) => {
375
- const index = Number(key.replace('param_', ''));
376
- const param = routeConfig.params.find(
377
- (it) => it.index === index
378
- );
379
-
380
- if (param?.screen === routeName && param?.name) {
381
- return [param.name, value];
382
- }
383
-
384
- return null;
385
- })
386
- .filter((it) => it != null)
387
- .map(([key, value]) => {
388
- if (value == null) {
389
- return [key, undefined];
390
- }
391
-
392
- const decoded = decodeURIComponent(value);
393
- const parsed = routeConfig.parse?.[key]
394
- ? routeConfig.parse[key](decoded)
395
- : decoded;
396
-
397
- return [key, parsed];
398
- })
399
- )
400
- : undefined;
502
+ let params: Record<string, unknown> | undefined;
503
+
504
+ if (routeConfig && match.groups) {
505
+ const paramEntries: [string, unknown][] = [];
506
+
507
+ for (const [key, value] of Object.entries(match.groups)) {
508
+ const index = Number(key.replace(PARAM_GROUP_PREFIX, ''));
509
+ const param = routeConfig.params.find((it) => it.index === index);
510
+
511
+ if (param?.screen !== routeName || !param.name) {
512
+ continue;
513
+ }
401
514
 
402
- if (params && Object.keys(params).length) {
403
- return { name: routeName, params };
515
+ if (value == null) {
516
+ paramEntries.push([param.name, undefined]);
517
+ continue;
404
518
  }
405
519
 
406
- return { name: routeName };
407
- });
520
+ let decoded: string;
408
521
 
409
- remainingPath = remainingPath.replace(match[0], '');
522
+ try {
523
+ decoded = decodeURIComponent(value);
524
+ } catch {
525
+ return undefined;
526
+ }
410
527
 
411
- break;
528
+ const parser = routeConfig.parse?.[param.name];
529
+
530
+ if (!parser) {
531
+ paramEntries.push([param.name, decoded]);
532
+ continue;
533
+ }
534
+
535
+ const schema = getStandardSchema(parser);
536
+
537
+ if (schema) {
538
+ const result = getValidationResult(schema, decoded);
539
+
540
+ if (result.issues) {
541
+ validationFailed = true;
542
+ break;
543
+ }
544
+
545
+ paramEntries.push([param.name, result.value]);
546
+ continue;
547
+ }
548
+
549
+ if (typeof parser === 'function') {
550
+ paramEntries.push([param.name, parser(decoded)]);
551
+ continue;
552
+ }
553
+
554
+ throw new Error(INVALID_PARSER_ERROR);
555
+ }
556
+
557
+ if (validationFailed) {
558
+ // A failed param validation invalidates the whole nested route chain for this config.
559
+ break;
560
+ }
561
+
562
+ if (paramEntries.length) {
563
+ params = Object.fromEntries(paramEntries);
564
+ }
565
+ }
566
+
567
+ if (params && Object.keys(params).length) {
568
+ matchedRoutes.push({ name: routeName, params });
569
+ } else {
570
+ matchedRoutes.push({ name: routeName });
412
571
  }
413
572
  }
414
573
 
415
- return { routes, remainingPath };
574
+ if (validationFailed) {
575
+ return undefined;
576
+ }
577
+
578
+ return matchedRoutes;
416
579
  };
417
580
 
418
581
  const createNormalizedConfigs = (
419
582
  screen: string,
420
583
  routeConfig: Record<string, string | PathConfig<{}>>,
421
584
  initials: InitialRouteConfig[],
422
- paths: { screen: string; path: string }[],
585
+ parts: RoutePatternPart[],
423
586
  parentScreens: string[],
424
587
  routeNames: string[]
425
588
  ): RouteConfig[] => {
@@ -432,19 +595,28 @@ const createNormalizedConfigs = (
432
595
  const config = routeConfig[screen];
433
596
 
434
597
  if (typeof config === 'string') {
435
- paths.push({ screen, path: config });
436
- configs.push(createConfigItem(screen, [...routeNames], [...paths]));
598
+ const configParts = combinePatternParts(
599
+ parts,
600
+ getPatternParts(config).map((part) => ({ ...part, screen }))
601
+ );
602
+
603
+ configs.push(createConfigItem(screen, [...routeNames], configParts));
437
604
  } else if (typeof config === 'object') {
438
605
  // if an object is specified as the value (e.g. Foo: { ... }),
439
606
  // it can have `path` property and
440
607
  // it could have `screens` prop which has nested configs
441
- if (typeof config.path === 'string') {
442
- if (config.exact && config.path == null) {
443
- throw new Error(
444
- `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: ''\`.`
445
- );
446
- }
608
+ const nestedScreens = 'screens' in config ? config.screens : undefined;
609
+ const hasNestedScreens = !!nestedScreens;
610
+ const currentParts =
611
+ typeof config.path === 'string'
612
+ ? combinePatternParts(
613
+ parts,
614
+ getPatternParts(config.path).map((part) => ({ ...part, screen })),
615
+ config.exact
616
+ )
617
+ : parts;
447
618
 
619
+ if (typeof config.path === 'string') {
448
620
  // We should add alias configs after the main config
449
621
  // So unless they are more specific, main config will be matched first
450
622
  const aliasConfigs = [];
@@ -456,8 +628,13 @@ const createNormalizedConfigs = (
456
628
  createConfigItem(
457
629
  screen,
458
630
  [...routeNames],
459
- [...paths, { screen, path: alias }],
460
- config.parse
631
+ combinePatternParts(
632
+ parts,
633
+ getPatternParts(alias).map((part) => ({ ...part, screen }))
634
+ ),
635
+ config.parse,
636
+ hasNestedScreens,
637
+ false
461
638
  )
462
639
  );
463
640
  } else if (typeof alias === 'object') {
@@ -465,25 +642,32 @@ const createNormalizedConfigs = (
465
642
  createConfigItem(
466
643
  screen,
467
644
  [...routeNames],
468
- alias.exact
469
- ? [{ screen, path: alias.path }]
470
- : [...paths, { screen, path: alias.path }],
471
- alias.parse
645
+ combinePatternParts(
646
+ parts,
647
+ getPatternParts(alias.path).map((part) => ({
648
+ ...part,
649
+ screen,
650
+ })),
651
+ alias.exact
652
+ ),
653
+ alias.parse,
654
+ hasNestedScreens,
655
+ alias.shared === true
472
656
  )
473
657
  );
474
658
  }
475
659
  }
476
660
  }
477
661
 
478
- if (config.exact) {
479
- // If it's an exact path, we don't need to keep track of the parent screens
480
- // So we can clear it
481
- paths.length = 0;
482
- }
483
-
484
- paths.push({ screen, path: config.path });
485
662
  configs.push(
486
- createConfigItem(screen, [...routeNames], [...paths], config.parse)
663
+ createConfigItem(
664
+ screen,
665
+ [...routeNames],
666
+ currentParts,
667
+ config.parse,
668
+ hasNestedScreens,
669
+ config.shared === true
670
+ )
487
671
  );
488
672
 
489
673
  configs.push(...aliasConfigs);
@@ -499,7 +683,7 @@ const createNormalizedConfigs = (
499
683
  );
500
684
  }
501
685
 
502
- if ('screens' in config && config.screens) {
686
+ if (hasNestedScreens) {
503
687
  // property `initialRouteName` without `screens` has no purpose
504
688
  if (
505
689
  'initialRouteName' in config &&
@@ -511,12 +695,12 @@ const createNormalizedConfigs = (
511
695
  });
512
696
  }
513
697
 
514
- Object.keys(config.screens).forEach((nestedConfig) => {
698
+ Object.keys(nestedScreens).forEach((nestedConfig) => {
515
699
  const result = createNormalizedConfigs(
516
700
  nestedConfig,
517
- config.screens as Record<string, string | PathConfig<{}>>,
701
+ nestedScreens as Record<string, string | PathConfig<{}>>,
518
702
  initials,
519
- [...paths],
703
+ currentParts,
520
704
  [...parentScreens],
521
705
  routeNames
522
706
  );
@@ -534,16 +718,11 @@ const createNormalizedConfigs = (
534
718
  const createConfigItem = (
535
719
  screen: string,
536
720
  routeNames: string[],
537
- paths: { screen: string; path: string }[],
538
- parse?: ParseConfig
721
+ parts: RoutePatternPart[],
722
+ parse?: ParseConfig,
723
+ hasNestedScreens = false,
724
+ shared = false
539
725
  ): RouteConfig => {
540
- const parts: (PatternPart & { screen: string })[] = [];
541
-
542
- // Parse the path string into parts for easier matching
543
- for (const { screen, path } of paths) {
544
- parts.push(...getPatternParts(path).map((part) => ({ ...part, screen })));
545
- }
546
-
547
726
  const regex = parts.length
548
727
  ? new RegExp(
549
728
  `^(${parts
@@ -551,7 +730,7 @@ const createConfigItem = (
551
730
  if (it.param) {
552
731
  const reg = it.regex || '[^/]+';
553
732
 
554
- return `(((?<param_${i}>${reg})\\/)${it.optional ? '?' : ''})`;
733
+ return `(((?<${PARAM_GROUP_PREFIX}${i}>${reg})\\/)${it.optional ? '?' : ''})`;
555
734
  }
556
735
 
557
736
  return `${it.segment === '*' ? '.*' : escape(it.segment)}\\/`;
@@ -575,25 +754,95 @@ const createConfigItem = (
575
754
 
576
755
  return {
577
756
  screen,
757
+ order: 0,
578
758
  regex,
579
759
  segments,
580
760
  params,
581
761
  routeNames,
582
762
  parse,
763
+ explicitParamNames: getExplicitParamNames(parse),
764
+ hasNestedScreens,
765
+ shared,
583
766
  };
584
767
  };
585
768
 
586
- const findParseConfigForRoute = (
587
- routeName: string,
588
- flatConfig: RouteConfig[]
589
- ): ParseConfig | undefined => {
590
- for (const config of flatConfig) {
591
- if (routeName === config.routeNames[config.routeNames.length - 1]) {
592
- return config.parse;
769
+ const selectSharedConfig = (
770
+ match: RouteConfig,
771
+ candidates: RouteConfig[],
772
+ previous: NavigationState | PartialState<NavigationState> | undefined,
773
+ initialRoutes: InitialRouteConfig[]
774
+ ) => {
775
+ // Walk the previous state down to the focused leaf to get the active route path
776
+ const focusedRouteNames: string[] = [];
777
+
778
+ let current = previous;
779
+
780
+ while (current?.routes.length) {
781
+ const index =
782
+ typeof current.index === 'number'
783
+ ? current.index
784
+ : current.routes.length - 1;
785
+
786
+ const route = current.routes[index];
787
+
788
+ if (route == null) {
789
+ break;
593
790
  }
791
+
792
+ focusedRouteNames.push(route.name);
793
+ current = route.state;
594
794
  }
595
795
 
596
- return undefined;
796
+ // Rank a candidate by how far it aligns with the focused path, then by
797
+ // how many of its levels match a configured initial route
798
+ const score = (routeNames: string[]) => {
799
+ let focused = 0;
800
+
801
+ while (
802
+ routeNames[focused] !== undefined &&
803
+ routeNames[focused] === focusedRouteNames[focused]
804
+ ) {
805
+ focused++;
806
+ }
807
+
808
+ let initial = 0;
809
+
810
+ for (const config of initialRoutes) {
811
+ if (
812
+ arrayStartsWith(routeNames, config.parentScreens) &&
813
+ routeNames[config.parentScreens.length] === config.initialRouteName
814
+ ) {
815
+ initial++;
816
+ }
817
+ }
818
+
819
+ return { focused, initial };
820
+ };
821
+
822
+ let selected = match;
823
+ let best = { focused: -1, initial: -1 };
824
+
825
+ for (const candidate of candidates) {
826
+ if (!candidate.shared) {
827
+ continue;
828
+ }
829
+
830
+ const { focused, initial } = score(candidate.routeNames);
831
+
832
+ const better =
833
+ focused !== best.focused
834
+ ? focused > best.focused
835
+ : initial !== best.initial
836
+ ? initial > best.initial
837
+ : candidate.order < selected.order;
838
+
839
+ if (better) {
840
+ selected = candidate;
841
+ best = { focused, initial };
842
+ }
843
+ }
844
+
845
+ return selected;
597
846
  };
598
847
 
599
848
  // Try to find an initial route connected with the one passed
@@ -606,7 +855,14 @@ const findInitialRoute = (
606
855
  if (parentScreens.length === config.parentScreens.length) {
607
856
  let sameParents = true;
608
857
  for (let i = 0; i < parentScreens.length; i++) {
609
- if (parentScreens[i].localeCompare(config.parentScreens[i]) !== 0) {
858
+ const parentScreen = parentScreens[i];
859
+ const configParentScreen = config.parentScreens[i];
860
+
861
+ if (
862
+ parentScreen == null ||
863
+ configParentScreen == null ||
864
+ parentScreen.localeCompare(configParentScreen) !== 0
865
+ ) {
610
866
  sameParents = false;
611
867
  break;
612
868
  }
@@ -657,9 +913,14 @@ const createNestedStateObject = (
657
913
  path: string,
658
914
  routes: ParsedRoute[],
659
915
  initialRoutes: InitialRouteConfig[],
660
- flatConfig?: RouteConfig[]
661
- ) => {
662
- let route = routes.shift() as ParsedRoute;
916
+ routeConfig?: RouteConfig
917
+ ): InitialState | undefined => {
918
+ let route = routes.shift();
919
+
920
+ if (route == null) {
921
+ return undefined;
922
+ }
923
+
663
924
  const parentScreens: string[] = [];
664
925
 
665
926
  let initialRoute = findInitialRoute(route.name, parentScreens, initialRoutes);
@@ -675,57 +936,171 @@ const createNestedStateObject = (
675
936
  if (routes.length > 0) {
676
937
  let nestedState = state;
677
938
 
678
- while ((route = routes.shift() as ParsedRoute)) {
939
+ let nextRoute = routes.shift();
940
+
941
+ while (nextRoute != null) {
942
+ route = nextRoute;
679
943
  initialRoute = findInitialRoute(route.name, parentScreens, initialRoutes);
680
944
 
681
945
  const nestedStateIndex =
682
946
  nestedState.index || nestedState.routes.length - 1;
947
+ const nestedRoute = nestedState.routes[nestedStateIndex];
683
948
 
684
- nestedState.routes[nestedStateIndex].state = createStateObject(
949
+ if (nestedRoute == null) {
950
+ throw new Error(`Couldn't find a route at index ${nestedStateIndex}.`);
951
+ }
952
+
953
+ nestedRoute.state = createStateObject(
685
954
  initialRoute,
686
955
  route,
687
956
  routes.length === 0
688
957
  );
689
958
 
690
959
  if (routes.length > 0) {
691
- nestedState = nestedState.routes[nestedStateIndex]
692
- .state as InitialState;
960
+ if (nestedRoute.state == null) {
961
+ throw new Error(
962
+ `Couldn't find nested state for route '${route.name}'.`
963
+ );
964
+ }
965
+
966
+ nestedState = nestedRoute.state;
693
967
  }
694
968
 
695
969
  parentScreens.push(route.name);
970
+ nextRoute = routes.shift();
696
971
  }
697
972
  }
698
973
 
699
- route = findFocusedRoute(state) as ParsedRoute;
700
- route.path = path.replace(/\/$/, '');
974
+ const focusedRoute = findFocusedRoute(state);
975
+
976
+ if (focusedRoute == null) {
977
+ return undefined;
978
+ }
701
979
 
702
- const params = parseQueryParams(
980
+ focusedRoute.path = path.replace(/\/$/, '');
981
+
982
+ const pathParamNames = new Set(
983
+ routeConfig?.params
984
+ .filter(
985
+ (param): param is { screen: string; name: string; index: number } =>
986
+ param.screen === routeConfig.screen && typeof param.name === 'string'
987
+ )
988
+ .map((param) => param.name)
989
+ );
990
+
991
+ const queryParams = parseQueryParams(
703
992
  path,
704
- flatConfig ? findParseConfigForRoute(route.name, flatConfig) : undefined
993
+ routeConfig?.parse,
994
+ pathParamNames,
995
+ routeConfig?.explicitParamNames,
996
+ routeConfig?.hasNestedScreens,
997
+ focusedRoute.params
705
998
  );
706
999
 
707
- if (params) {
708
- route.params = { ...route.params, ...params };
1000
+ if (!queryParams.valid) {
1001
+ return undefined;
1002
+ }
1003
+
1004
+ if (queryParams.params) {
1005
+ focusedRoute.params = { ...focusedRoute.params, ...queryParams.params };
709
1006
  }
710
1007
 
711
1008
  return state;
712
1009
  };
713
1010
 
714
- const parseQueryParams = (path: string, parseConfig?: ParseConfig) => {
1011
+ const parseQueryParams = (
1012
+ path: string,
1013
+ parseConfig?: ParseConfig,
1014
+ pathParamNames: Set<string> = new Set(),
1015
+ explicitParamNames?: Set<string>,
1016
+ hasNestedScreens = false,
1017
+ routeParams?: object
1018
+ ):
1019
+ | { valid: true; params?: Record<string, unknown> | undefined }
1020
+ | { valid: false } => {
715
1021
  const query = path.split('?')[1];
716
- const params: Record<string, unknown> = queryString.parse(query);
1022
+ const params: Record<string, unknown> = query ? queryString.parse(query) : {};
1023
+
1024
+ // Path params should always win over same-named query params.
1025
+ for (const name of pathParamNames) {
1026
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
1027
+ delete params[name];
1028
+ }
717
1029
 
718
1030
  if (parseConfig) {
719
- Object.keys(params).forEach((name) => {
720
- if (
721
- Object.hasOwnProperty.call(parseConfig, name) &&
722
- parseConfig[name] &&
723
- typeof params[name] === 'string'
724
- ) {
725
- params[name] = parseConfig[name](params[name]);
1031
+ for (const [name, parser] of Object.entries(parseConfig)) {
1032
+ if (!parser || pathParamNames.has(name)) {
1033
+ continue;
726
1034
  }
727
- });
1035
+
1036
+ const schema = getStandardSchema(parser);
1037
+
1038
+ if (!Object.hasOwn(params, name)) {
1039
+ if (!schema) {
1040
+ continue;
1041
+ }
1042
+
1043
+ const result = getValidationResult(schema, undefined);
1044
+
1045
+ if (result.issues) {
1046
+ return { valid: false };
1047
+ }
1048
+
1049
+ if (result.value !== undefined) {
1050
+ params[name] = result.value;
1051
+ }
1052
+
1053
+ continue;
1054
+ }
1055
+
1056
+ if (schema) {
1057
+ const result = getValidationResult(schema, params[name]);
1058
+
1059
+ if (result.issues) {
1060
+ return { valid: false };
1061
+ }
1062
+
1063
+ params[name] = result.value;
1064
+ continue;
1065
+ }
1066
+
1067
+ const value = Array.isArray(params[name])
1068
+ ? params[name][0]
1069
+ : params[name];
1070
+
1071
+ if (typeof value !== 'string') {
1072
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
1073
+ delete params[name];
1074
+ continue;
1075
+ }
1076
+
1077
+ if (typeof parser === 'function') {
1078
+ params[name] = parser(value);
1079
+ continue;
1080
+ }
1081
+
1082
+ throw new Error(INVALID_PARSER_ERROR);
1083
+ }
728
1084
  }
729
1085
 
730
- return Object.keys(params).length ? params : undefined;
1086
+ if (
1087
+ hasNestedScreens &&
1088
+ !explicitParamNames?.has('screen') &&
1089
+ (typeof params.screen === 'string' ||
1090
+ (routeParams != null &&
1091
+ 'screen' in routeParams &&
1092
+ typeof routeParams.screen === 'string'))
1093
+ ) {
1094
+ for (const name of NESTED_SCREEN_PARAM_NAMES) {
1095
+ if (!explicitParamNames?.has(name)) {
1096
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
1097
+ delete params[name];
1098
+ }
1099
+ }
1100
+ }
1101
+
1102
+ return {
1103
+ valid: true,
1104
+ params: Object.keys(params).length ? params : undefined,
1105
+ };
731
1106
  };