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

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