@react-navigation/core 8.0.0-alpha.3 → 8.0.0-alpha.30

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 (283) hide show
  1. package/lib/module/BaseNavigationContainer.js +34 -17
  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/DataLoading.js +127 -0
  6. package/lib/module/DataLoading.js.map +1 -0
  7. package/lib/module/NavigationBuilderContext.js +1 -0
  8. package/lib/module/NavigationBuilderContext.js.map +1 -1
  9. package/lib/module/NavigationIndependentTree.js +17 -8
  10. package/lib/module/NavigationIndependentTree.js.map +1 -1
  11. package/lib/module/NavigationProvider.js +22 -11
  12. package/lib/module/NavigationProvider.js.map +1 -1
  13. package/lib/module/NavigationStateContext.js.map +1 -1
  14. package/lib/module/PreventRemoveContext.js.map +1 -1
  15. package/lib/module/PreventRemoveProvider.js +37 -27
  16. package/lib/module/PreventRemoveProvider.js.map +1 -1
  17. package/lib/module/SceneView.js +7 -40
  18. package/lib/module/SceneView.js.map +1 -1
  19. package/lib/module/StaticNavigation.js +179 -50
  20. package/lib/module/StaticNavigation.js.map +1 -1
  21. package/lib/module/StaticTreeContext.js +5 -0
  22. package/lib/module/StaticTreeContext.js.map +1 -0
  23. package/lib/module/checkSerializable.js +18 -13
  24. package/lib/module/checkSerializable.js.map +1 -1
  25. package/lib/module/createNavigationContainerRef.js +6 -8
  26. package/lib/module/createNavigationContainerRef.js.map +1 -1
  27. package/lib/module/createNavigatorFactory.js +27 -5
  28. package/lib/module/createNavigatorFactory.js.map +1 -1
  29. package/lib/module/findFocusedRoute.js +7 -4
  30. package/lib/module/findFocusedRoute.js.map +1 -1
  31. package/lib/module/getActionFromState.js +45 -20
  32. package/lib/module/getActionFromState.js.map +1 -1
  33. package/lib/module/getPathFromState.js +171 -90
  34. package/lib/module/getPathFromState.js.map +1 -1
  35. package/lib/module/getPatternParts.js +20 -4
  36. package/lib/module/getPatternParts.js.map +1 -1
  37. package/lib/module/getStateFromPath.js +485 -214
  38. package/lib/module/getStateFromPath.js.map +1 -1
  39. package/lib/module/getStateFromRouteParams.js +24 -0
  40. package/lib/module/getStateFromRouteParams.js.map +1 -0
  41. package/lib/module/index.js +5 -4
  42. package/lib/module/index.js.map +1 -1
  43. package/lib/module/theming/useTheme.js +1 -1
  44. package/lib/module/theming/useTheme.js.map +1 -1
  45. package/lib/module/types.js +35 -0
  46. package/lib/module/types.js.map +1 -1
  47. package/lib/module/useDescriptors.js +19 -11
  48. package/lib/module/useDescriptors.js.map +1 -1
  49. package/lib/module/useEventEmitter.js +48 -33
  50. package/lib/module/useEventEmitter.js.map +1 -1
  51. package/lib/module/useFocusEvents.js +12 -4
  52. package/lib/module/useFocusEvents.js.map +1 -1
  53. package/lib/module/useFocusedListenersChildrenAdapter.js +2 -2
  54. package/lib/module/useFocusedListenersChildrenAdapter.js.map +1 -1
  55. package/lib/module/useIsFocused.js +7 -12
  56. package/lib/module/useIsFocused.js.map +1 -1
  57. package/lib/module/useKeyedChildListeners.js +4 -2
  58. package/lib/module/useKeyedChildListeners.js.map +1 -1
  59. package/lib/module/useLazyValue.js.map +1 -1
  60. package/lib/module/useNavigation.js +2 -15
  61. package/lib/module/useNavigation.js.map +1 -1
  62. package/lib/module/useNavigationBuilder.js +201 -131
  63. package/lib/module/useNavigationBuilder.js.map +1 -1
  64. package/lib/module/useNavigationCache.js +22 -17
  65. package/lib/module/useNavigationCache.js.map +1 -1
  66. package/lib/module/useNavigationHelpers.js +28 -10
  67. package/lib/module/useNavigationHelpers.js.map +1 -1
  68. package/lib/module/useNavigationIndependentTree.js +1 -1
  69. package/lib/module/useNavigationIndependentTree.js.map +1 -1
  70. package/lib/module/useNavigationState.js +70 -35
  71. package/lib/module/useNavigationState.js.map +1 -1
  72. package/lib/module/useOnAction.js +37 -7
  73. package/lib/module/useOnAction.js.map +1 -1
  74. package/lib/module/useOnGetState.js +6 -3
  75. package/lib/module/useOnGetState.js.map +1 -1
  76. package/lib/module/useOnPreventRemove.js +39 -30
  77. package/lib/module/useOnPreventRemove.js.map +1 -1
  78. package/lib/module/useOnRouteFocus.js +1 -1
  79. package/lib/module/useOnRouteFocus.js.map +1 -1
  80. package/lib/module/useOptionsGetters.js +10 -5
  81. package/lib/module/useOptionsGetters.js.map +1 -1
  82. package/lib/module/usePreventRemove.js +10 -3
  83. package/lib/module/usePreventRemove.js.map +1 -1
  84. package/lib/module/usePreventRemoveContext.js +1 -1
  85. package/lib/module/usePreventRemoveContext.js.map +1 -1
  86. package/lib/module/useRegisterNavigator.js +2 -2
  87. package/lib/module/useRegisterNavigator.js.map +1 -1
  88. package/lib/module/useRoute.js +2 -0
  89. package/lib/module/useRoute.js.map +1 -1
  90. package/lib/module/useRouteCache.js +17 -11
  91. package/lib/module/useRouteCache.js.map +1 -1
  92. package/lib/module/useScheduleUpdate.js +9 -9
  93. package/lib/module/useScheduleUpdate.js.map +1 -1
  94. package/lib/module/useStateForPath.js +1 -1
  95. package/lib/module/useStateForPath.js.map +1 -1
  96. package/lib/module/useSyncState.js +7 -3
  97. package/lib/module/useSyncState.js.map +1 -1
  98. package/lib/module/validatePathConfig.js +50 -36
  99. package/lib/module/validatePathConfig.js.map +1 -1
  100. package/lib/typescript/src/BaseNavigationContainer.d.ts +6 -3
  101. package/lib/typescript/src/BaseNavigationContainer.d.ts.map +1 -1
  102. package/lib/typescript/src/ConsumedParamsContext.d.ts +3 -0
  103. package/lib/typescript/src/ConsumedParamsContext.d.ts.map +1 -0
  104. package/lib/typescript/src/DataLoading.d.ts +21 -0
  105. package/lib/typescript/src/DataLoading.d.ts.map +1 -0
  106. package/lib/typescript/src/EnsureSingleNavigator.d.ts +1 -1
  107. package/lib/typescript/src/EnsureSingleNavigator.d.ts.map +1 -1
  108. package/lib/typescript/src/Group.d.ts +1 -1
  109. package/lib/typescript/src/Group.d.ts.map +1 -1
  110. package/lib/typescript/src/NavigationBuilderContext.d.ts +9 -8
  111. package/lib/typescript/src/NavigationBuilderContext.d.ts.map +1 -1
  112. package/lib/typescript/src/NavigationContainerRefContext.d.ts +1 -1
  113. package/lib/typescript/src/NavigationContainerRefContext.d.ts.map +1 -1
  114. package/lib/typescript/src/NavigationFocusedRouteStateContext.d.ts +4 -4
  115. package/lib/typescript/src/NavigationFocusedRouteStateContext.d.ts.map +1 -1
  116. package/lib/typescript/src/NavigationHelpersContext.d.ts +1 -1
  117. package/lib/typescript/src/NavigationHelpersContext.d.ts.map +1 -1
  118. package/lib/typescript/src/NavigationIndependentTree.d.ts +1 -1
  119. package/lib/typescript/src/NavigationIndependentTree.d.ts.map +1 -1
  120. package/lib/typescript/src/NavigationProvider.d.ts +12 -13
  121. package/lib/typescript/src/NavigationProvider.d.ts.map +1 -1
  122. package/lib/typescript/src/NavigationStateContext.d.ts +3 -3
  123. package/lib/typescript/src/NavigationStateContext.d.ts.map +1 -1
  124. package/lib/typescript/src/PreventRemoveContext.d.ts +1 -0
  125. package/lib/typescript/src/PreventRemoveContext.d.ts.map +1 -1
  126. package/lib/typescript/src/PreventRemoveProvider.d.ts +1 -1
  127. package/lib/typescript/src/PreventRemoveProvider.d.ts.map +1 -1
  128. package/lib/typescript/src/SceneView.d.ts +3 -2
  129. package/lib/typescript/src/SceneView.d.ts.map +1 -1
  130. package/lib/typescript/src/Screen.d.ts +1 -1
  131. package/lib/typescript/src/Screen.d.ts.map +1 -1
  132. package/lib/typescript/src/StaticContainer.d.ts +1 -1
  133. package/lib/typescript/src/StaticContainer.d.ts.map +1 -1
  134. package/lib/typescript/src/StaticNavigation.d.ts +189 -109
  135. package/lib/typescript/src/StaticNavigation.d.ts.map +1 -1
  136. package/lib/typescript/src/StaticTreeContext.d.ts +4 -0
  137. package/lib/typescript/src/StaticTreeContext.d.ts.map +1 -0
  138. package/lib/typescript/src/checkSerializable.d.ts +8 -4
  139. package/lib/typescript/src/checkSerializable.d.ts.map +1 -1
  140. package/lib/typescript/src/createNavigationContainerRef.d.ts +1 -1
  141. package/lib/typescript/src/createNavigationContainerRef.d.ts.map +1 -1
  142. package/lib/typescript/src/createNavigatorFactory.d.ts +23 -5
  143. package/lib/typescript/src/createNavigatorFactory.d.ts.map +1 -1
  144. package/lib/typescript/src/findFocusedRoute.d.ts +3 -3
  145. package/lib/typescript/src/findFocusedRoute.d.ts.map +1 -1
  146. package/lib/typescript/src/getActionFromState.d.ts +4 -4
  147. package/lib/typescript/src/getActionFromState.d.ts.map +1 -1
  148. package/lib/typescript/src/getPathFromState.d.ts +3 -3
  149. package/lib/typescript/src/getPathFromState.d.ts.map +1 -1
  150. package/lib/typescript/src/getPatternParts.d.ts +1 -0
  151. package/lib/typescript/src/getPatternParts.d.ts.map +1 -1
  152. package/lib/typescript/src/getStateFromPath.d.ts +5 -5
  153. package/lib/typescript/src/getStateFromPath.d.ts.map +1 -1
  154. package/lib/typescript/src/getStateFromRouteParams.d.ts +3 -0
  155. package/lib/typescript/src/getStateFromRouteParams.d.ts.map +1 -0
  156. package/lib/typescript/src/index.d.ts +36 -33
  157. package/lib/typescript/src/index.d.ts.map +1 -1
  158. package/lib/typescript/src/theming/ThemeContext.d.ts +1 -1
  159. package/lib/typescript/src/theming/ThemeContext.d.ts.map +1 -1
  160. package/lib/typescript/src/theming/ThemeProvider.d.ts +2 -2
  161. package/lib/typescript/src/theming/ThemeProvider.d.ts.map +1 -1
  162. package/lib/typescript/src/theming/useTheme.d.ts +1 -1
  163. package/lib/typescript/src/theming/useTheme.d.ts.map +1 -1
  164. package/lib/typescript/src/types.d.ts +260 -159
  165. package/lib/typescript/src/types.d.ts.map +1 -1
  166. package/lib/typescript/src/useChildListeners.d.ts +3 -3
  167. package/lib/typescript/src/useChildListeners.d.ts.map +1 -1
  168. package/lib/typescript/src/useComponent.d.ts +1 -1
  169. package/lib/typescript/src/useComponent.d.ts.map +1 -1
  170. package/lib/typescript/src/useDescriptors.d.ts +8 -8
  171. package/lib/typescript/src/useDescriptors.d.ts.map +1 -1
  172. package/lib/typescript/src/useEventEmitter.d.ts +2 -2
  173. package/lib/typescript/src/useEventEmitter.d.ts.map +1 -1
  174. package/lib/typescript/src/useFocusEvents.d.ts +2 -2
  175. package/lib/typescript/src/useFocusEvents.d.ts.map +1 -1
  176. package/lib/typescript/src/useFocusedListenersChildrenAdapter.d.ts +2 -2
  177. package/lib/typescript/src/useFocusedListenersChildrenAdapter.d.ts.map +1 -1
  178. package/lib/typescript/src/useIsFocused.d.ts +3 -0
  179. package/lib/typescript/src/useIsFocused.d.ts.map +1 -1
  180. package/lib/typescript/src/useKeyedChildListeners.d.ts +3 -3
  181. package/lib/typescript/src/useKeyedChildListeners.d.ts.map +1 -1
  182. package/lib/typescript/src/useLazyValue.d.ts.map +1 -1
  183. package/lib/typescript/src/useNavigation.d.ts +4 -2
  184. package/lib/typescript/src/useNavigation.d.ts.map +1 -1
  185. package/lib/typescript/src/useNavigationBuilder.d.ts +23 -30
  186. package/lib/typescript/src/useNavigationBuilder.d.ts.map +1 -1
  187. package/lib/typescript/src/useNavigationCache.d.ts +3 -3
  188. package/lib/typescript/src/useNavigationCache.d.ts.map +1 -1
  189. package/lib/typescript/src/useNavigationContainerRef.d.ts +1 -1
  190. package/lib/typescript/src/useNavigationContainerRef.d.ts.map +1 -1
  191. package/lib/typescript/src/useNavigationHelpers.d.ts +19 -27
  192. package/lib/typescript/src/useNavigationHelpers.d.ts.map +1 -1
  193. package/lib/typescript/src/useNavigationState.d.ts +13 -3
  194. package/lib/typescript/src/useNavigationState.d.ts.map +1 -1
  195. package/lib/typescript/src/useOnAction.d.ts +9 -9
  196. package/lib/typescript/src/useOnAction.d.ts.map +1 -1
  197. package/lib/typescript/src/useOnGetState.d.ts +1 -1
  198. package/lib/typescript/src/useOnGetState.d.ts.map +1 -1
  199. package/lib/typescript/src/useOnPreventRemove.d.ts +5 -9
  200. package/lib/typescript/src/useOnPreventRemove.d.ts.map +1 -1
  201. package/lib/typescript/src/useOnRouteFocus.d.ts +6 -6
  202. package/lib/typescript/src/useOnRouteFocus.d.ts.map +1 -1
  203. package/lib/typescript/src/useOptionsGetters.d.ts +1 -1
  204. package/lib/typescript/src/useOptionsGetters.d.ts.map +1 -1
  205. package/lib/typescript/src/usePreventRemove.d.ts.map +1 -1
  206. package/lib/typescript/src/usePreventRemoveContext.d.ts +2 -1
  207. package/lib/typescript/src/usePreventRemoveContext.d.ts.map +1 -1
  208. package/lib/typescript/src/useRoute.d.ts +7 -3
  209. package/lib/typescript/src/useRoute.d.ts.map +1 -1
  210. package/lib/typescript/src/useRouteCache.d.ts +5 -5
  211. package/lib/typescript/src/useRouteCache.d.ts.map +1 -1
  212. package/lib/typescript/src/useScheduleUpdate.d.ts +5 -4
  213. package/lib/typescript/src/useScheduleUpdate.d.ts.map +1 -1
  214. package/lib/typescript/src/useStateForPath.d.ts +1 -1
  215. package/lib/typescript/src/useStateForPath.d.ts.map +1 -1
  216. package/lib/typescript/src/useSyncState.d.ts.map +1 -1
  217. package/lib/typescript/src/utilities.d.ts +74 -37
  218. package/lib/typescript/src/utilities.d.ts.map +1 -1
  219. package/lib/typescript/src/validatePathConfig.d.ts.map +1 -1
  220. package/package.json +23 -22
  221. package/src/BaseNavigationContainer.tsx +351 -333
  222. package/src/ConsumedParamsContext.tsx +5 -0
  223. package/src/DataLoading.tsx +203 -0
  224. package/src/NavigationBuilderContext.tsx +14 -9
  225. package/src/NavigationFocusedRouteStateContext.tsx +4 -4
  226. package/src/NavigationIndependentTree.tsx +18 -12
  227. package/src/NavigationProvider.tsx +26 -15
  228. package/src/NavigationStateContext.tsx +5 -6
  229. package/src/PreventRemoveContext.tsx +1 -0
  230. package/src/PreventRemoveProvider.tsx +51 -46
  231. package/src/SceneView.tsx +7 -39
  232. package/src/StaticNavigation.tsx +659 -307
  233. package/src/StaticTreeContext.tsx +7 -0
  234. package/src/checkSerializable.tsx +35 -19
  235. package/src/createNavigationContainerRef.tsx +12 -9
  236. package/src/createNavigatorFactory.tsx +92 -8
  237. package/src/findFocusedRoute.tsx +13 -8
  238. package/src/getActionFromState.tsx +86 -51
  239. package/src/getPathFromState.tsx +254 -128
  240. package/src/getPatternParts.tsx +25 -4
  241. package/src/getStateFromPath.tsx +811 -309
  242. package/src/getStateFromRouteParams.tsx +60 -0
  243. package/src/index.tsx +55 -5
  244. package/src/theming/useTheme.tsx +1 -1
  245. package/src/types.tsx +798 -373
  246. package/src/useDescriptors.tsx +28 -14
  247. package/src/useEventEmitter.tsx +52 -38
  248. package/src/useFocusEvents.tsx +15 -7
  249. package/src/useFocusedListenersChildrenAdapter.tsx +2 -2
  250. package/src/useIsFocused.tsx +14 -21
  251. package/src/useKeyedChildListeners.tsx +4 -2
  252. package/src/useLazyValue.tsx +2 -2
  253. package/src/useNavigation.tsx +7 -24
  254. package/src/useNavigationBuilder.tsx +311 -186
  255. package/src/useNavigationCache.tsx +32 -25
  256. package/src/useNavigationHelpers.tsx +27 -10
  257. package/src/useNavigationIndependentTree.tsx +1 -1
  258. package/src/useNavigationState.tsx +93 -59
  259. package/src/useOnAction.tsx +61 -13
  260. package/src/useOnGetState.tsx +5 -3
  261. package/src/useOnPreventRemove.tsx +75 -42
  262. package/src/useOnRouteFocus.tsx +10 -12
  263. package/src/useOptionsGetters.tsx +11 -5
  264. package/src/usePreventRemove.tsx +12 -3
  265. package/src/usePreventRemoveContext.tsx +1 -1
  266. package/src/useRegisterNavigator.tsx +2 -2
  267. package/src/useRoute.tsx +20 -6
  268. package/src/useRouteCache.tsx +22 -16
  269. package/src/useScheduleUpdate.tsx +9 -10
  270. package/src/useStateForPath.tsx +1 -1
  271. package/src/useSyncState.tsx +8 -3
  272. package/src/utilities.tsx +144 -31
  273. package/src/validatePathConfig.tsx +86 -57
  274. package/lib/module/CurrentRenderContext.js +0 -10
  275. package/lib/module/CurrentRenderContext.js.map +0 -1
  276. package/lib/module/useCurrentRender.js +0 -19
  277. package/lib/module/useCurrentRender.js.map +0 -1
  278. package/lib/typescript/src/CurrentRenderContext.d.ts +0 -9
  279. package/lib/typescript/src/CurrentRenderContext.d.ts.map +0 -1
  280. package/lib/typescript/src/useCurrentRender.d.ts +0 -14
  281. package/lib/typescript/src/useCurrentRender.d.ts.map +0 -1
  282. package/src/CurrentRenderContext.tsx +0 -9
  283. package/src/useCurrentRender.tsx +0 -31
@@ -1,12 +1,46 @@
1
1
  "use strict";
2
2
 
3
3
  import escape from 'escape-string-regexp';
4
- import * as queryString from 'query-string';
4
+ import queryString from 'query-string';
5
5
  import { arrayStartsWith } from "./arrayStartsWith.js";
6
6
  import { findFocusedRoute } from "./findFocusedRoute.js";
7
- import { getPatternParts } from "./getPatternParts.js";
7
+ import { combinePatternParts, getPatternParts } from "./getPatternParts.js";
8
8
  import { isArrayEqual } from "./isArrayEqual.js";
9
9
  import { validatePathConfig } from "./validatePathConfig.js";
10
+ const INVALID_SCHEMA_RESULT_ERROR = 'Invalid validation result from schema. It should be an object with either "value" or "issues" property and cannot be asynchronous.';
11
+ const INVALID_PARSER_ERROR = 'Invalid parser. Expected a function or a Standard Schema V1 object.';
12
+ const PARAM_GROUP_PREFIX = 'param_';
13
+ const NESTED_SCREEN_PARAM_NAMES = ['screen', 'params', 'initial', 'path', 'merge', 'pop'];
14
+ const getStaticSegmentPattern = segment => Array.from(segment, char => {
15
+ const encoded = encodeURIComponent(char);
16
+ const percentEncoded = encoded === char ? `%${char.charCodeAt(0).toString(16).padStart(2, '0').toUpperCase()}` : encoded;
17
+ return `(?:${escape(char)}|${escape(percentEncoded)})`;
18
+ }).join('');
19
+ const getExplicitParamNames = parse => {
20
+ const names = [];
21
+ if (parse) {
22
+ for (const name in parse) {
23
+ if (parse[name] != null) {
24
+ names.push(name);
25
+ }
26
+ }
27
+ }
28
+ return names.length ? new Set(names) : undefined;
29
+ };
30
+ const getStandardSchema = parser => {
31
+ if ('~standard' in parser && typeof parser['~standard'] === 'object' && parser['~standard'] !== null && 'version' in parser['~standard'] && parser['~standard'].version === 1 && 'validate' in parser['~standard'] && typeof parser['~standard'].validate === 'function') {
32
+ return parser['~standard'];
33
+ }
34
+ return undefined;
35
+ };
36
+ const getValidationResult = (schema, value) => {
37
+ const result = schema.validate(value);
38
+ if (result != null && typeof result === 'object' && ('value' in result || 'issues' in result && Array.isArray(result.issues))) {
39
+ return result;
40
+ }
41
+ throw new Error(INVALID_SCHEMA_RESULT_ERROR);
42
+ };
43
+
10
44
  /**
11
45
  * Utility to parse a path string to initial state object accepted by the container.
12
46
  * This is useful for deep linking when we need to handle the incoming URL.
@@ -28,40 +62,56 @@ import { validatePathConfig } from "./validatePathConfig.js";
28
62
  * @param path Path string to parse and convert, e.g. /foo/bar?count=42.
29
63
  * @param options Extra options to fine-tune how to parse the path.
30
64
  */
31
- export function getStateFromPath(path, options) {
65
+ export function getStateFromPath(path, options, previous) {
32
66
  const {
33
67
  initialRoutes,
34
- configs
68
+ configs,
69
+ configsByScreen,
70
+ configsByPattern,
71
+ prefixRegex
35
72
  } = getConfigResources(options);
36
73
  const screens = options?.screens;
74
+
75
+ // When a matched config is shared, the same pattern can resolve to multiple branches
76
+ // pick the one that best matches the current navigation state
77
+ const resolveSharedConfig = config => {
78
+ if (config.shared) {
79
+ const candidates = configsByPattern[config.pattern];
80
+ if (candidates) {
81
+ return selectSharedConfig(config, candidates, previous, initialRoutes);
82
+ }
83
+ }
84
+ return config;
85
+ };
37
86
  let remaining = path.replace(/\/+/g, '/') // Replace multiple slash (//) with single ones
38
87
  .replace(/^\//, '') // Remove extra leading slash
39
- .replace(/\?.*$/, ''); // Remove query params which we will handle later
88
+ .replace(/\?.*$/, '') // Remove query params which we will handle later
89
+ .replace(/%[0-9a-f]{2}/gi, m => m.toUpperCase()); // Normalize percent-encoding as hex digits are case-insensitive
40
90
 
41
91
  // Make sure there is a trailing slash
42
92
  remaining = remaining.endsWith('/') ? remaining : `${remaining}/`;
43
- const prefix = options?.path?.replace(/^\//, ''); // Remove extra leading slash
44
-
45
- if (prefix) {
46
- // Make sure there is a trailing slash
47
- const normalizedPrefix = prefix.endsWith('/') ? prefix : `${prefix}/`;
48
-
49
- // If the path doesn't start with the prefix, it's not a match
50
- if (!remaining.startsWith(normalizedPrefix)) {
93
+ if (prefixRegex) {
94
+ const prefixMatch = remaining.match(prefixRegex);
95
+ if (prefixMatch == null) {
51
96
  return undefined;
52
97
  }
53
-
54
- // Remove the prefix from the path
55
- remaining = remaining.replace(normalizedPrefix, '');
98
+ remaining = remaining.slice(prefixMatch[0].length);
56
99
  }
57
100
  if (screens === undefined) {
58
101
  // When no config is specified, use the path segments as route names
59
- const routes = remaining.split('/').filter(Boolean).map(segment => {
60
- const name = decodeURIComponent(segment);
61
- return {
62
- name
63
- };
64
- });
102
+ const routes = [];
103
+ for (const segment of remaining.split('/')) {
104
+ if (!segment) {
105
+ continue;
106
+ }
107
+ try {
108
+ routes.push({
109
+ name: decodeURIComponent(segment)
110
+ });
111
+ } catch {
112
+ return undefined;
113
+ }
114
+ }
65
115
  if (routes.length) {
66
116
  return createNestedStateObject(path, routes, initialRoutes);
67
117
  }
@@ -70,59 +120,109 @@ export function getStateFromPath(path, options) {
70
120
  if (remaining === '/') {
71
121
  // We need to add special handling of empty path so navigation to empty path also works
72
122
  // When handling empty path, we should only look at the root level config
73
- const match = configs.find(config => config.segments.join('/') === '');
123
+ const match = configs.find(config => config.pattern === '');
74
124
  if (match) {
75
- return createNestedStateObject(path, match.routeNames.map(name => ({
125
+ const config = resolveSharedConfig(match);
126
+ return createNestedStateObject(path, config.routeNames.map(name => ({
76
127
  name
77
- })), initialRoutes, configs);
128
+ })), initialRoutes, config);
78
129
  }
79
130
  return undefined;
80
131
  }
81
- let result;
82
- let current;
83
132
 
84
133
  // We match the whole path against the regex instead of segments
85
134
  // This makes sure matches such as wildcard will catch any unmatched routes, even if nested
86
- const {
87
- routes,
88
- remainingPath
89
- } = matchAgainstConfigs(remaining, configs);
90
- if (routes !== undefined) {
91
- // This will always be empty if full path matched
92
- current = createNestedStateObject(path, routes, initialRoutes, configs);
93
- remaining = remainingPath;
94
- result = current;
95
- }
96
- if (current == null || result == null) {
97
- return undefined;
135
+ const firstRawSegment = remaining.split('/')[0];
136
+ let firstDecodedSegment;
137
+ try {
138
+ firstDecodedSegment = firstRawSegment != null ? decodeURIComponent(firstRawSegment) : undefined;
139
+ } catch {
140
+ firstDecodedSegment = undefined;
98
141
  }
99
- return result;
142
+ for (const config of configs) {
143
+ // Regex matching is expensive, so skip configs that can't match this path.
144
+ if (!canMatchFirstSegment(config.segments[0], firstDecodedSegment, firstRawSegment)) {
145
+ continue;
146
+ }
147
+ const routes = matchAgainstConfig(remaining, config, configsByScreen);
148
+ if (routes === undefined) {
149
+ continue;
150
+ }
151
+ let selectedConfig = resolveSharedConfig(config);
152
+ let selectedRoutes = routes;
153
+ if (selectedConfig !== config) {
154
+ const rematchedRoutes = matchAgainstConfig(remaining, selectedConfig, configsByScreen);
155
+ if (rematchedRoutes === undefined) {
156
+ selectedConfig = config;
157
+ } else {
158
+ selectedRoutes = rematchedRoutes;
159
+ }
160
+ }
161
+ const state = createNestedStateObject(path, selectedRoutes, initialRoutes, selectedConfig);
162
+ if (state !== undefined) {
163
+ return state;
164
+ }
165
+ }
166
+ return undefined;
100
167
  }
168
+ const canMatchFirstSegment = (configSegment, decodedSegment, rawSegment) => {
169
+ if (decodedSegment === undefined) {
170
+ return true;
171
+ }
172
+ if (configSegment === undefined) {
173
+ return false;
174
+ }
175
+ if (configSegment === '*' || configSegment.startsWith(':')) {
176
+ return true;
177
+ }
178
+ return configSegment === decodedSegment || configSegment === rawSegment;
179
+ };
101
180
 
102
181
  /**
103
182
  * Reference to the last used config resources. This is used to avoid recomputing the config resources when the options are the same.
104
183
  */
105
184
  const cachedConfigResources = new WeakMap();
106
185
  function getConfigResources(options) {
107
- if (!options) return prepareConfigResources();
186
+ if (!options) {
187
+ return prepareConfigResources();
188
+ }
108
189
  const cached = cachedConfigResources.get(options);
109
- if (cached) return cached;
190
+ if (cached) {
191
+ return cached;
192
+ }
193
+ validatePathConfig(options);
110
194
  const resources = prepareConfigResources(options);
111
195
  cachedConfigResources.set(options, resources);
112
196
  return resources;
113
197
  }
114
198
  function prepareConfigResources(options) {
115
- if (options) {
116
- validatePathConfig(options);
117
- }
118
199
  const initialRoutes = getInitialRoutes(options);
119
200
  const configs = getSortedNormalizedConfigs(initialRoutes, options?.screens);
120
- checkForDuplicatedConfigs(configs);
121
- const configWithRegexes = getConfigsWithRegexes(configs);
201
+ const prefix = options?.path?.replace(/^\//, '');
202
+ let prefixRegex;
203
+ if (prefix) {
204
+ const normalizedPrefix = prefix.endsWith('/') ? prefix : `${prefix}/`;
205
+ const prefixPattern = normalizedPrefix.split('/').map(getStaticSegmentPattern).join('/');
206
+ prefixRegex = new RegExp(`^${prefixPattern}`);
207
+ }
208
+ const configsByScreen = {};
209
+ const configsByPattern = {};
210
+ for (const c of configs) {
211
+ (configsByScreen[c.screen] ??= []).push(c);
212
+ const existing = configsByPattern[c.pattern];
213
+ if (existing) {
214
+ checkForDuplicatedConfigs(existing[existing.length - 1], c);
215
+ existing.push(c);
216
+ } else {
217
+ configsByPattern[c.pattern] = [c];
218
+ }
219
+ }
122
220
  return {
123
221
  initialRoutes,
124
222
  configs,
125
- configWithRegexes
223
+ configsByScreen,
224
+ configsByPattern,
225
+ prefixRegex
126
226
  };
127
227
  }
128
228
  function getInitialRoutes(options) {
@@ -137,7 +237,17 @@ function getInitialRoutes(options) {
137
237
  }
138
238
  function getSortedNormalizedConfigs(initialRoutes, screens = {}) {
139
239
  // Create a normalized configs array which will be easier to use
140
- return [].concat(...Object.keys(screens).map(key => createNormalizedConfigs(key, screens, initialRoutes, [], [], []))).sort((a, b) => {
240
+ const configs = [];
241
+ for (const key in screens) {
242
+ configs.push(...createNormalizedConfigs(key, screens, initialRoutes, [], [], []));
243
+ }
244
+ for (let order = 0; order < configs.length; order++) {
245
+ const config = configs[order];
246
+ if (config) {
247
+ config.order = order;
248
+ }
249
+ }
250
+ return configs.sort((a, b) => {
141
251
  // Sort config from most specific to least specific:
142
252
  // - more segments
143
253
  // - static segments
@@ -148,7 +258,13 @@ function getSortedNormalizedConfigs(initialRoutes, screens = {}) {
148
258
  // If 2 patterns are same, move the one with less route names up
149
259
  // This is an error state, so it's only useful for consistent error messages
150
260
  if (isArrayEqual(a.segments, b.segments)) {
151
- return b.routeNames.join('>').localeCompare(a.routeNames.join('>'));
261
+ if (a.routeNames.length > b.routeNames.length && arrayStartsWith(a.routeNames, b.routeNames)) {
262
+ return -1;
263
+ }
264
+ if (b.routeNames.length > a.routeNames.length && arrayStartsWith(b.routeNames, a.routeNames)) {
265
+ return 1;
266
+ }
267
+ return a.routeNames.length - b.routeNames.length || a.order - b.order;
152
268
  }
153
269
 
154
270
  // If one of the patterns starts with the other, it's more exhaustive
@@ -160,21 +276,24 @@ function getSortedNormalizedConfigs(initialRoutes, screens = {}) {
160
276
  return 1;
161
277
  }
162
278
  for (let i = 0; i < Math.max(a.segments.length, b.segments.length); i++) {
279
+ const aSegment = a.segments[i];
280
+ const bSegment = b.segments[i];
281
+
163
282
  // if b is longer, b gets higher priority
164
- if (a.segments[i] == null) {
283
+ if (aSegment == null) {
165
284
  return 1;
166
285
  }
167
286
 
168
287
  // if a is longer, a gets higher priority
169
- if (b.segments[i] == null) {
288
+ if (bSegment == null) {
170
289
  return -1;
171
290
  }
172
- const aWildCard = a.segments[i] === '*';
173
- const bWildCard = b.segments[i] === '*';
174
- const aParam = a.segments[i].startsWith(':');
175
- const bParam = b.segments[i].startsWith(':');
176
- const aRegex = aParam && a.segments[i].includes('(');
177
- const bRegex = bParam && b.segments[i].includes('(');
291
+ const aWildCard = aSegment === '*';
292
+ const bWildCard = bSegment === '*';
293
+ const aParam = aSegment.startsWith(':');
294
+ const bParam = bSegment.startsWith(':');
295
+ const aRegex = aParam && aSegment.includes('(');
296
+ const bRegex = bParam && bSegment.includes('(');
178
297
 
179
298
  // if both are wildcard or regex, we compare next component
180
299
  if (aWildCard && bWildCard || aRegex && bRegex) {
@@ -214,142 +333,154 @@ function getSortedNormalizedConfigs(initialRoutes, screens = {}) {
214
333
  return a.segments.length - b.segments.length;
215
334
  });
216
335
  }
217
- function checkForDuplicatedConfigs(configs) {
218
- // Check for duplicate patterns in the config
219
- configs.reduce((acc, config) => {
220
- const pattern = config.segments.join('/');
221
- if (acc[pattern]) {
222
- const a = acc[pattern].routeNames;
223
- const b = config.routeNames;
224
336
 
225
- // It's not a problem if the path string omitted from a inner most screen
226
- // For example, it's ok if a path resolves to `A > B > C` or `A > B`
227
- const intersects = a.length > b.length ? b.every((it, i) => a[i] === it) : a.every((it, i) => b[i] === it);
228
- if (!intersects) {
229
- throw new Error(`Found conflicting screens with the same pattern. The pattern '${pattern}' resolves to both '${a.join(' > ')}' and '${b.join(' > ')}'. Patterns must be unique and cannot resolve to more than one screen.`);
230
- }
231
- }
232
- return Object.assign(acc, {
233
- [pattern]: config
234
- });
235
- }, {});
236
- }
237
- function getConfigsWithRegexes(configs) {
238
- return configs.map(c => ({
239
- ...c,
240
- // Add `$` to the regex to make sure it matches till end of the path and not just beginning
241
- regex: c.regex ? new RegExp(c.regex.source + '$') : undefined
242
- }));
243
- }
244
- const matchAgainstConfigs = (remaining, configs) => {
245
- let routes;
246
- let remainingPath = remaining;
337
+ // Throw if two configs resolve to the same pattern but conflicting screens
338
+ function checkForDuplicatedConfigs(existing, config) {
339
+ if (!existing) {
340
+ return;
341
+ }
342
+ const a = existing.routeNames;
343
+ const b = config.routeNames;
247
344
 
248
- // Go through all configs, and see if the next path segment matches our regex
249
- for (const config of configs) {
250
- if (!config.regex) {
251
- continue;
252
- }
253
- const match = remainingPath.match(config.regex);
345
+ // It's not a problem if the path string omitted from a inner most screen
346
+ // For example, it's ok if a path resolves to `A > B > C` or `A > B`
347
+ const intersects = a.length > b.length ? arrayStartsWith(a, b) : arrayStartsWith(b, a);
348
+ if (!intersects && (!existing.shared || !config.shared)) {
349
+ 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 unless shared: true is specified.`);
350
+ }
351
+ }
352
+ const matchAgainstConfig = (remaining, config, configsByScreen) => {
353
+ if (!config.regex) {
354
+ return undefined;
355
+ }
356
+ const match = remaining.match(config.regex);
357
+ if (!match) {
358
+ return undefined;
359
+ }
360
+ let validationFailed = false;
361
+ const matchedRoutes = [];
362
+ for (const [index, routeName] of config.routeNames.entries()) {
363
+ // Check matching name, route names and pattern in case same screen is used
364
+ // at different levels or branches in config
365
+ const routeConfig = configsByScreen[routeName]?.find(c => index === c.routeNames.length - 1 && arrayStartsWith(config.routeNames, c.routeNames) && arrayStartsWith(config.segments, c.segments));
366
+ let params;
367
+ let hasParams = false;
368
+ if (routeConfig && match.groups) {
369
+ params = {};
370
+ for (const param of routeConfig.params) {
371
+ if (param.screen !== routeName) {
372
+ continue;
373
+ }
374
+ const value = match.groups[`${PARAM_GROUP_PREFIX}${param.index}`];
375
+ if (value == null) {
376
+ params[param.name] = undefined;
377
+ hasParams = true;
378
+ continue;
379
+ }
380
+ let decoded;
381
+ try {
382
+ decoded = decodeURIComponent(value);
383
+ } catch {
384
+ return undefined;
385
+ }
254
386
 
255
- // If our regex matches, we need to extract params from the path
256
- if (match) {
257
- routes = config.routeNames.map(routeName => {
258
- const routeConfig = configs.find(c => {
259
- // Check matching name AND pattern in case same screen is used at different levels in config
260
- return c.screen === routeName && arrayStartsWith(config.segments, c.segments);
261
- });
262
- const params = routeConfig && match.groups ? Object.fromEntries(Object.entries(match.groups).map(([key, value]) => {
263
- const index = Number(key.replace('param_', ''));
264
- const param = routeConfig.params.find(it => it.index === index);
265
- if (param?.screen === routeName && param?.name) {
266
- return [param.name, value];
267
- }
268
- return null;
269
- }).filter(it => it != null).map(([key, value]) => {
270
- if (value == null) {
271
- return [key, undefined];
387
+ // Percent-encoded values are matched permissively
388
+ // So validate the decoded value against the custom regex
389
+ if (param.regex && value !== decoded && !param.regex.test(decoded)) {
390
+ return undefined;
391
+ }
392
+ const parser = routeConfig.parse?.[param.name];
393
+ if (!parser) {
394
+ params[param.name] = decoded;
395
+ hasParams = true;
396
+ continue;
397
+ }
398
+ const schema = getStandardSchema(parser);
399
+ if (schema) {
400
+ const result = getValidationResult(schema, decoded);
401
+ if (result.issues) {
402
+ validationFailed = true;
403
+ break;
272
404
  }
273
- const decoded = decodeURIComponent(value);
274
- const parsed = routeConfig.parse?.[key] ? routeConfig.parse[key](decoded) : decoded;
275
- return [key, parsed];
276
- })) : undefined;
277
- if (params && Object.keys(params).length) {
278
- return {
279
- name: routeName,
280
- params
281
- };
405
+ params[param.name] = result.value;
406
+ hasParams = true;
407
+ continue;
408
+ }
409
+ if (typeof parser === 'function') {
410
+ params[param.name] = parser(decoded);
411
+ hasParams = true;
412
+ continue;
282
413
  }
283
- return {
284
- name: routeName
285
- };
414
+ throw new Error(INVALID_PARSER_ERROR);
415
+ }
416
+ if (validationFailed) {
417
+ // A failed param validation invalidates the whole nested route chain for this config.
418
+ break;
419
+ }
420
+ }
421
+ if (params && hasParams) {
422
+ matchedRoutes.push({
423
+ name: routeName,
424
+ params
425
+ });
426
+ } else {
427
+ matchedRoutes.push({
428
+ name: routeName
286
429
  });
287
- remainingPath = remainingPath.replace(match[0], '');
288
- break;
289
430
  }
290
431
  }
291
- return {
292
- routes,
293
- remainingPath
294
- };
432
+ if (validationFailed) {
433
+ return undefined;
434
+ }
435
+ return matchedRoutes;
295
436
  };
296
- const createNormalizedConfigs = (screen, routeConfig, initials, paths, parentScreens, routeNames) => {
437
+ const createNormalizedConfigs = (screen, routeConfig, initials, parts, parentScreens, routeNames) => {
297
438
  const configs = [];
298
439
  routeNames.push(screen);
299
440
  parentScreens.push(screen);
300
441
  const config = routeConfig[screen];
301
442
  if (typeof config === 'string') {
302
- paths.push({
303
- screen,
304
- path: config
305
- });
306
- configs.push(createConfigItem(screen, [...routeNames], [...paths]));
443
+ const configParts = combinePatternParts(parts, getPatternParts(config).map(part => ({
444
+ ...part,
445
+ screen
446
+ })));
447
+ configs.push(createConfigItem(screen, [...routeNames], configParts));
307
448
  } else if (typeof config === 'object') {
308
449
  // if an object is specified as the value (e.g. Foo: { ... }),
309
450
  // it can have `path` property and
310
451
  // it could have `screens` prop which has nested configs
452
+ const nestedScreens = 'screens' in config ? config.screens : undefined;
453
+ const hasNestedScreens = !!nestedScreens;
454
+ const currentParts = typeof config.path === 'string' ? combinePatternParts(parts, getPatternParts(config.path).map(part => ({
455
+ ...part,
456
+ screen
457
+ })), config.exact) : parts;
311
458
  if (typeof config.path === 'string') {
312
- if (config.exact && config.path == null) {
313
- 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: ''\`.`);
314
- }
315
-
316
459
  // We should add alias configs after the main config
317
460
  // So unless they are more specific, main config will be matched first
318
461
  const aliasConfigs = [];
319
462
  if (config.alias) {
320
463
  for (const alias of config.alias) {
321
464
  if (typeof alias === 'string') {
322
- aliasConfigs.push(createConfigItem(screen, [...routeNames], [...paths, {
323
- screen,
324
- path: alias
325
- }], config.parse));
465
+ aliasConfigs.push(createConfigItem(screen, [...routeNames], combinePatternParts(parts, getPatternParts(alias).map(part => ({
466
+ ...part,
467
+ screen
468
+ }))), config.parse, hasNestedScreens, false));
326
469
  } else if (typeof alias === 'object') {
327
- aliasConfigs.push(createConfigItem(screen, [...routeNames], alias.exact ? [{
328
- screen,
329
- path: alias.path
330
- }] : [...paths, {
331
- screen,
332
- path: alias.path
333
- }], alias.parse));
470
+ aliasConfigs.push(createConfigItem(screen, [...routeNames], combinePatternParts(parts, getPatternParts(alias.path).map(part => ({
471
+ ...part,
472
+ screen
473
+ })), alias.exact), alias.parse, hasNestedScreens, alias.shared === true));
334
474
  }
335
475
  }
336
476
  }
337
- if (config.exact) {
338
- // If it's an exact path, we don't need to keep track of the parent screens
339
- // So we can clear it
340
- paths.length = 0;
341
- }
342
- paths.push({
343
- screen,
344
- path: config.path
345
- });
346
- configs.push(createConfigItem(screen, [...routeNames], [...paths], config.parse));
477
+ configs.push(createConfigItem(screen, [...routeNames], currentParts, config.parse, hasNestedScreens, config.shared === true));
347
478
  configs.push(...aliasConfigs);
348
479
  }
349
480
  if (typeof config !== 'string' && typeof config.path !== 'string' && config.alias?.length) {
350
481
  throw new Error(`Screen '${screen}' doesn't specify a 'path'. A 'path' needs to be specified in order to use 'alias'.`);
351
482
  }
352
- if ('screens' in config && config.screens) {
483
+ if (nestedScreens) {
353
484
  // property `initialRouteName` without `screens` has no purpose
354
485
  if ('initialRouteName' in config && typeof config.initialRouteName === 'string') {
355
486
  initials.push({
@@ -357,57 +488,120 @@ const createNormalizedConfigs = (screen, routeConfig, initials, paths, parentScr
357
488
  parentScreens
358
489
  });
359
490
  }
360
- Object.keys(config.screens).forEach(nestedConfig => {
361
- const result = createNormalizedConfigs(nestedConfig, config.screens, initials, [...paths], [...parentScreens], routeNames);
491
+ for (const nestedConfig in nestedScreens) {
492
+ const result = createNormalizedConfigs(nestedConfig, nestedScreens, initials, currentParts, [...parentScreens], routeNames);
362
493
  configs.push(...result);
363
- });
494
+ }
364
495
  }
365
496
  }
366
497
  routeNames.pop();
367
498
  return configs;
368
499
  };
369
- const createConfigItem = (screen, routeNames, paths, parse) => {
370
- const parts = [];
371
-
372
- // Parse the path string into parts for easier matching
373
- for (const {
374
- screen,
375
- path
376
- } of paths) {
377
- parts.push(...getPatternParts(path).map(part => ({
378
- ...part,
379
- screen
380
- })));
500
+ const createConfigItem = (screen, routeNames, parts, parse, hasNestedScreens = false, shared = false) => {
501
+ let regex;
502
+ const segments = [];
503
+ const params = [];
504
+ const pathParamNames = new Set();
505
+ if (parts.length) {
506
+ let regexString = '^(';
507
+ let index = 0;
508
+ for (const part of parts) {
509
+ segments.push(part.segment);
510
+ if (part.param) {
511
+ // A custom regex may not match the percent-encoded form of the value
512
+ // So also accept segments containing an encoded character and validate them after decoding
513
+ const reg = part.regex ? `(?:${part.regex})|(?=[^/]*%[0-9A-F]{2})[^/]+` : '[^/]+';
514
+ regexString += `(((?<${PARAM_GROUP_PREFIX}${index}>${reg})\\/)${part.optional ? '?' : ''})`;
515
+ params.push({
516
+ index,
517
+ screen: part.screen,
518
+ name: part.param,
519
+ regex: part.regex ? new RegExp(`^(?:${part.regex})$`) : undefined
520
+ });
521
+ if (part.screen === screen) {
522
+ pathParamNames.add(part.param);
523
+ }
524
+ } else if (part.segment === '*') {
525
+ regexString += `.*\\/`;
526
+ } else {
527
+ regexString += `${getStaticSegmentPattern(part.segment)}\\/`;
528
+ }
529
+ index++;
530
+ }
531
+ regexString += ')$';
532
+ regex = new RegExp(regexString);
381
533
  }
382
- const regex = parts.length ? new RegExp(`^(${parts.map((it, i) => {
383
- if (it.param) {
384
- const reg = it.regex || '[^/]+';
385
- return `(((?<param_${i}>${reg})\\/)${it.optional ? '?' : ''})`;
386
- }
387
- return `${it.segment === '*' ? '.*' : escape(it.segment)}\\/`;
388
- }).join('')})$`) : undefined;
389
- const segments = parts.map(it => it.segment);
390
- const params = parts.map((it, i) => it.param ? {
391
- index: i,
392
- screen: it.screen,
393
- name: it.param
394
- } : null).filter(it => it != null);
534
+ const pattern = segments.join('/');
395
535
  return {
396
536
  screen,
537
+ order: 0,
397
538
  regex,
539
+ pattern,
398
540
  segments,
399
541
  params,
400
542
  routeNames,
401
- parse
543
+ parse,
544
+ explicitParamNames: getExplicitParamNames(parse),
545
+ pathParamNames,
546
+ hasNestedScreens,
547
+ shared
402
548
  };
403
549
  };
404
- const findParseConfigForRoute = (routeName, flatConfig) => {
405
- for (const config of flatConfig) {
406
- if (routeName === config.routeNames[config.routeNames.length - 1]) {
407
- return config.parse;
550
+ const selectSharedConfig = (match, candidates, previous, initialRoutes) => {
551
+ // Walk the previous state down to the focused leaf to get the active route path
552
+ const focusedRouteNames = [];
553
+ let current = previous;
554
+ while (current?.routes.length) {
555
+ const index = typeof current.index === 'number' ? current.index : current.routes.length - 1;
556
+ const route = current.routes[index];
557
+ if (route == null) {
558
+ break;
408
559
  }
560
+ focusedRouteNames.push(route.name);
561
+ current = route.state;
409
562
  }
410
- return undefined;
563
+
564
+ // Rank a candidate by how far it aligns with the focused path, then by
565
+ // how many of its levels match a configured initial route
566
+ const score = routeNames => {
567
+ let focused = 0;
568
+ while (routeNames[focused] !== undefined && routeNames[focused] === focusedRouteNames[focused]) {
569
+ focused++;
570
+ }
571
+ let initial = 0;
572
+ for (const config of initialRoutes) {
573
+ if (arrayStartsWith(routeNames, config.parentScreens) && routeNames[config.parentScreens.length] === config.initialRouteName) {
574
+ initial++;
575
+ }
576
+ }
577
+ return {
578
+ focused,
579
+ initial
580
+ };
581
+ };
582
+ let selected = match;
583
+ let best = {
584
+ focused: -1,
585
+ initial: -1
586
+ };
587
+ for (const candidate of candidates) {
588
+ if (!candidate.shared) {
589
+ continue;
590
+ }
591
+ const {
592
+ focused,
593
+ initial
594
+ } = score(candidate.routeNames);
595
+ const better = focused !== best.focused ? focused > best.focused : initial !== best.initial ? initial > best.initial : candidate.order < selected.order;
596
+ if (better) {
597
+ selected = candidate;
598
+ best = {
599
+ focused,
600
+ initial
601
+ };
602
+ }
603
+ }
604
+ return selected;
411
605
  };
412
606
 
413
607
  // Try to find an initial route connected with the one passed
@@ -416,7 +610,9 @@ const findInitialRoute = (routeName, parentScreens, initialRoutes) => {
416
610
  if (parentScreens.length === config.parentScreens.length) {
417
611
  let sameParents = true;
418
612
  for (let i = 0; i < parentScreens.length; i++) {
419
- if (parentScreens[i].localeCompare(config.parentScreens[i]) !== 0) {
613
+ const parentScreen = parentScreens[i];
614
+ const configParentScreen = config.parentScreens[i];
615
+ if (parentScreen == null || configParentScreen == null || parentScreen.localeCompare(configParentScreen) !== 0) {
420
616
  sameParents = false;
421
617
  break;
422
618
  }
@@ -470,45 +666,120 @@ const createStateObject = (initialRoute, route, isEmpty) => {
470
666
  }
471
667
  }
472
668
  };
473
- const createNestedStateObject = (path, routes, initialRoutes, flatConfig) => {
669
+ const createNestedStateObject = (path, routes, initialRoutes, routeConfig) => {
474
670
  let route = routes.shift();
671
+ if (route == null) {
672
+ return undefined;
673
+ }
475
674
  const parentScreens = [];
476
675
  let initialRoute = findInitialRoute(route.name, parentScreens, initialRoutes);
477
676
  parentScreens.push(route.name);
478
677
  const state = createStateObject(initialRoute, route, routes.length === 0);
479
678
  if (routes.length > 0) {
480
679
  let nestedState = state;
481
- while (route = routes.shift()) {
680
+ let nextRoute = routes.shift();
681
+ while (nextRoute != null) {
682
+ route = nextRoute;
482
683
  initialRoute = findInitialRoute(route.name, parentScreens, initialRoutes);
483
684
  const nestedStateIndex = nestedState.index || nestedState.routes.length - 1;
484
- nestedState.routes[nestedStateIndex].state = createStateObject(initialRoute, route, routes.length === 0);
685
+ const nestedRoute = nestedState.routes[nestedStateIndex];
686
+ if (nestedRoute == null) {
687
+ throw new Error(`Couldn't find a route at index ${nestedStateIndex}.`);
688
+ }
689
+ nestedRoute.state = createStateObject(initialRoute, route, routes.length === 0);
485
690
  if (routes.length > 0) {
486
- nestedState = nestedState.routes[nestedStateIndex].state;
691
+ if (nestedRoute.state == null) {
692
+ throw new Error(`Couldn't find nested state for route '${route.name}'.`);
693
+ }
694
+ nestedState = nestedRoute.state;
487
695
  }
488
696
  parentScreens.push(route.name);
697
+ nextRoute = routes.shift();
489
698
  }
490
699
  }
491
- route = findFocusedRoute(state);
492
- route.path = path.replace(/\/$/, '');
493
- const params = parseQueryParams(path, flatConfig ? findParseConfigForRoute(route.name, flatConfig) : undefined);
494
- if (params) {
495
- route.params = {
496
- ...route.params,
497
- ...params
700
+ const focusedRoute = findFocusedRoute(state);
701
+ if (focusedRoute == null) {
702
+ return undefined;
703
+ }
704
+ focusedRoute.path = path.replace(/\/$/, '');
705
+ const queryParams = parseQueryParams(path, routeConfig?.parse, routeConfig?.pathParamNames, routeConfig?.explicitParamNames, routeConfig?.hasNestedScreens, focusedRoute.params);
706
+ if (!queryParams.valid) {
707
+ return undefined;
708
+ }
709
+ if (queryParams.params) {
710
+ focusedRoute.params = {
711
+ ...focusedRoute.params,
712
+ ...queryParams.params
498
713
  };
499
714
  }
500
715
  return state;
501
716
  };
502
- const parseQueryParams = (path, parseConfig) => {
503
- const query = path.split('?')[1];
504
- const params = queryString.parse(query);
717
+ const parseQueryParams = (path, parseConfig, pathParamNames = new Set(), explicitParamNames, hasNestedScreens = false, routeParams) => {
718
+ const queryIndex = path.indexOf('?');
719
+ const query = queryIndex === -1 ? undefined : path.slice(queryIndex + 1);
720
+ const params = query ? queryString.parse(query) : {};
721
+
722
+ // Path params should always win over same-named query params.
723
+ for (const name of pathParamNames) {
724
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
725
+ delete params[name];
726
+ }
505
727
  if (parseConfig) {
506
- Object.keys(params).forEach(name => {
507
- if (Object.hasOwnProperty.call(parseConfig, name) && parseConfig[name] && typeof params[name] === 'string') {
508
- params[name] = parseConfig[name](params[name]);
728
+ for (const name in parseConfig) {
729
+ const parser = parseConfig[name];
730
+ if (!parser || pathParamNames.has(name)) {
731
+ continue;
509
732
  }
510
- });
733
+ const schema = getStandardSchema(parser);
734
+ if (!Object.hasOwn(params, name)) {
735
+ if (!schema) {
736
+ continue;
737
+ }
738
+ const result = getValidationResult(schema, undefined);
739
+ if (result.issues) {
740
+ return {
741
+ valid: false
742
+ };
743
+ }
744
+ if (result.value !== undefined) {
745
+ params[name] = result.value;
746
+ }
747
+ continue;
748
+ }
749
+ if (schema) {
750
+ const result = getValidationResult(schema, params[name]);
751
+ if (result.issues) {
752
+ return {
753
+ valid: false
754
+ };
755
+ }
756
+ params[name] = result.value;
757
+ continue;
758
+ }
759
+ const value = Array.isArray(params[name]) ? params[name][0] : params[name];
760
+ if (typeof value !== 'string') {
761
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
762
+ delete params[name];
763
+ continue;
764
+ }
765
+ if (typeof parser === 'function') {
766
+ params[name] = parser(value);
767
+ continue;
768
+ }
769
+ throw new Error(INVALID_PARSER_ERROR);
770
+ }
511
771
  }
512
- return Object.keys(params).length ? params : undefined;
772
+ if (hasNestedScreens && !explicitParamNames?.has('screen') && (typeof params.screen === 'string' || routeParams != null && 'screen' in routeParams && typeof routeParams.screen === 'string')) {
773
+ for (const name of NESTED_SCREEN_PARAM_NAMES) {
774
+ if (!explicitParamNames?.has(name)) {
775
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
776
+ delete params[name];
777
+ }
778
+ }
779
+ }
780
+ return {
781
+ valid: true,
782
+ params: Object.keys(params).length ? params : undefined
783
+ };
513
784
  };
514
785
  //# sourceMappingURL=getStateFromPath.js.map