@react-navigation/core 8.0.0-alpha.4 → 8.0.0-alpha.41

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 (323) hide show
  1. package/README.md +17 -14
  2. package/lib/module/BaseNavigationContainer.js +173 -90
  3. package/lib/module/BaseNavigationContainer.js.map +1 -1
  4. package/lib/module/ConsumedParamsContext.js +5 -0
  5. package/lib/module/ConsumedParamsContext.js.map +1 -0
  6. package/lib/module/DataLoading.js +129 -0
  7. package/lib/module/DataLoading.js.map +1 -0
  8. package/lib/module/NavigationBuilderContext.js +7 -10
  9. package/lib/module/NavigationBuilderContext.js.map +1 -1
  10. package/lib/module/NavigationIndependentTree.js +54 -9
  11. package/lib/module/NavigationIndependentTree.js.map +1 -1
  12. package/lib/module/NavigationProvider.js +37 -12
  13. package/lib/module/NavigationProvider.js.map +1 -1
  14. package/lib/module/NavigationRootContext.js +8 -0
  15. package/lib/module/NavigationRootContext.js.map +1 -0
  16. package/lib/module/NavigationStateContext.js +3 -0
  17. package/lib/module/NavigationStateContext.js.map +1 -1
  18. package/lib/module/PreventRemoveContext.js.map +1 -1
  19. package/lib/module/PreventRemoveProvider.js +40 -31
  20. package/lib/module/PreventRemoveProvider.js.map +1 -1
  21. package/lib/module/SceneView.js +29 -57
  22. package/lib/module/SceneView.js.map +1 -1
  23. package/lib/module/StaticNavigation.js +219 -67
  24. package/lib/module/StaticNavigation.js.map +1 -1
  25. package/lib/module/StaticTreeContext.js +5 -0
  26. package/lib/module/StaticTreeContext.js.map +1 -0
  27. package/lib/module/createNavigationContainerRef.js +7 -9
  28. package/lib/module/createNavigationContainerRef.js.map +1 -1
  29. package/lib/module/createNavigatorFactory.js +27 -5
  30. package/lib/module/createNavigatorFactory.js.map +1 -1
  31. package/lib/module/findFocusedRoute.js +7 -4
  32. package/lib/module/findFocusedRoute.js.map +1 -1
  33. package/lib/module/getActionFromState.js +45 -20
  34. package/lib/module/getActionFromState.js.map +1 -1
  35. package/lib/module/getPathFromState.js +204 -94
  36. package/lib/module/getPathFromState.js.map +1 -1
  37. package/lib/module/getPatternParts.js +61 -14
  38. package/lib/module/getPatternParts.js.map +1 -1
  39. package/lib/module/getStateFromPath.js +608 -272
  40. package/lib/module/getStateFromPath.js.map +1 -1
  41. package/lib/module/getStateFromRouteParams.js +23 -0
  42. package/lib/module/getStateFromRouteParams.js.map +1 -0
  43. package/lib/module/index.js +7 -6
  44. package/lib/module/index.js.map +1 -1
  45. package/lib/module/internal.js +2 -0
  46. package/lib/module/internal.js.map +1 -0
  47. package/lib/module/isRecordEqual.js +1 -1
  48. package/lib/module/isRecordEqual.js.map +1 -1
  49. package/lib/module/queryString.js +71 -0
  50. package/lib/module/queryString.js.map +1 -0
  51. package/lib/module/theming/useTheme.js +1 -1
  52. package/lib/module/theming/useTheme.js.map +1 -1
  53. package/lib/module/types.js +35 -1
  54. package/lib/module/types.js.map +1 -1
  55. package/lib/module/useDescriptors.js +65 -25
  56. package/lib/module/useDescriptors.js.map +1 -1
  57. package/lib/module/useEventEmitter.js +48 -33
  58. package/lib/module/useEventEmitter.js.map +1 -1
  59. package/lib/module/useFocusEffect.js +4 -2
  60. package/lib/module/useFocusEffect.js.map +1 -1
  61. package/lib/module/useFocusEvents.js +15 -5
  62. package/lib/module/useFocusEvents.js.map +1 -1
  63. package/lib/module/useFocusedListenersChildrenAdapter.js +3 -3
  64. package/lib/module/useFocusedListenersChildrenAdapter.js.map +1 -1
  65. package/lib/module/useIsFocused.js +8 -12
  66. package/lib/module/useIsFocused.js.map +1 -1
  67. package/lib/module/useKeyedChildListeners.js +5 -2
  68. package/lib/module/useKeyedChildListeners.js.map +1 -1
  69. package/lib/module/useLazyValue.js.map +1 -1
  70. package/lib/module/useNavigation.js +4 -17
  71. package/lib/module/useNavigation.js.map +1 -1
  72. package/lib/module/useNavigationBuilder.js +244 -147
  73. package/lib/module/useNavigationBuilder.js.map +1 -1
  74. package/lib/module/useNavigationCache.js +92 -89
  75. package/lib/module/useNavigationCache.js.map +1 -1
  76. package/lib/module/useNavigationHelpers.js +22 -29
  77. package/lib/module/useNavigationHelpers.js.map +1 -1
  78. package/lib/module/useNavigationIndependentTree.js +1 -1
  79. package/lib/module/useNavigationIndependentTree.js.map +1 -1
  80. package/lib/module/useNavigationState.js +158 -46
  81. package/lib/module/useNavigationState.js.map +1 -1
  82. package/lib/module/useOnAction.js +58 -11
  83. package/lib/module/useOnAction.js.map +1 -1
  84. package/lib/module/useOnGetNavigation.js +26 -0
  85. package/lib/module/useOnGetNavigation.js.map +1 -0
  86. package/lib/module/useOnGetState.js +7 -4
  87. package/lib/module/useOnGetState.js.map +1 -1
  88. package/lib/module/useOnPreventRemove.js +40 -31
  89. package/lib/module/useOnPreventRemove.js.map +1 -1
  90. package/lib/module/useOnRouteFocus.js +2 -2
  91. package/lib/module/useOnRouteFocus.js.map +1 -1
  92. package/lib/module/useOptionsGetters.js +35 -27
  93. package/lib/module/useOptionsGetters.js.map +1 -1
  94. package/lib/module/usePreventRemove.js +11 -5
  95. package/lib/module/usePreventRemove.js.map +1 -1
  96. package/lib/module/usePreventRemoveContext.js +2 -2
  97. package/lib/module/usePreventRemoveContext.js.map +1 -1
  98. package/lib/module/useRegisterNavigator.js +3 -4
  99. package/lib/module/useRegisterNavigator.js.map +1 -1
  100. package/lib/module/useRoute.js +2 -0
  101. package/lib/module/useRoute.js.map +1 -1
  102. package/lib/module/useRouteCache.js +51 -35
  103. package/lib/module/useRouteCache.js.map +1 -1
  104. package/lib/module/useScheduleUpdate.js +12 -10
  105. package/lib/module/useScheduleUpdate.js.map +1 -1
  106. package/lib/module/useStateForPath.js +1 -1
  107. package/lib/module/useStateForPath.js.map +1 -1
  108. package/lib/module/useSyncState.js +37 -15
  109. package/lib/module/useSyncState.js.map +1 -1
  110. package/lib/module/validatePathConfig.js +50 -36
  111. package/lib/module/validatePathConfig.js.map +1 -1
  112. package/lib/typescript/src/BaseNavigationContainer.d.ts +6 -3
  113. package/lib/typescript/src/BaseNavigationContainer.d.ts.map +1 -1
  114. package/lib/typescript/src/ConsumedParamsContext.d.ts +3 -0
  115. package/lib/typescript/src/ConsumedParamsContext.d.ts.map +1 -0
  116. package/lib/typescript/src/DataLoading.d.ts +21 -0
  117. package/lib/typescript/src/DataLoading.d.ts.map +1 -0
  118. package/lib/typescript/src/EnsureSingleNavigator.d.ts +1 -1
  119. package/lib/typescript/src/EnsureSingleNavigator.d.ts.map +1 -1
  120. package/lib/typescript/src/Group.d.ts +1 -1
  121. package/lib/typescript/src/Group.d.ts.map +1 -1
  122. package/lib/typescript/src/NavigationBuilderContext.d.ts +22 -14
  123. package/lib/typescript/src/NavigationBuilderContext.d.ts.map +1 -1
  124. package/lib/typescript/src/NavigationContainerRefContext.d.ts +1 -1
  125. package/lib/typescript/src/NavigationContainerRefContext.d.ts.map +1 -1
  126. package/lib/typescript/src/NavigationFocusedRouteStateContext.d.ts +4 -4
  127. package/lib/typescript/src/NavigationFocusedRouteStateContext.d.ts.map +1 -1
  128. package/lib/typescript/src/NavigationHelpersContext.d.ts +1 -1
  129. package/lib/typescript/src/NavigationHelpersContext.d.ts.map +1 -1
  130. package/lib/typescript/src/NavigationIndependentTree.d.ts +1 -1
  131. package/lib/typescript/src/NavigationIndependentTree.d.ts.map +1 -1
  132. package/lib/typescript/src/NavigationProvider.d.ts +21 -18
  133. package/lib/typescript/src/NavigationProvider.d.ts.map +1 -1
  134. package/lib/typescript/src/NavigationRootContext.d.ts +8 -0
  135. package/lib/typescript/src/NavigationRootContext.d.ts.map +1 -0
  136. package/lib/typescript/src/NavigationStateContext.d.ts +3 -3
  137. package/lib/typescript/src/NavigationStateContext.d.ts.map +1 -1
  138. package/lib/typescript/src/PreventRemoveContext.d.ts +1 -0
  139. package/lib/typescript/src/PreventRemoveContext.d.ts.map +1 -1
  140. package/lib/typescript/src/PreventRemoveProvider.d.ts +1 -1
  141. package/lib/typescript/src/PreventRemoveProvider.d.ts.map +1 -1
  142. package/lib/typescript/src/SceneView.d.ts +5 -2
  143. package/lib/typescript/src/SceneView.d.ts.map +1 -1
  144. package/lib/typescript/src/Screen.d.ts +1 -1
  145. package/lib/typescript/src/Screen.d.ts.map +1 -1
  146. package/lib/typescript/src/StaticContainer.d.ts +1 -1
  147. package/lib/typescript/src/StaticContainer.d.ts.map +1 -1
  148. package/lib/typescript/src/StaticNavigation.d.ts +189 -111
  149. package/lib/typescript/src/StaticNavigation.d.ts.map +1 -1
  150. package/lib/typescript/src/StaticTreeContext.d.ts +4 -0
  151. package/lib/typescript/src/StaticTreeContext.d.ts.map +1 -0
  152. package/lib/typescript/src/createNavigationContainerRef.d.ts +1 -1
  153. package/lib/typescript/src/createNavigationContainerRef.d.ts.map +1 -1
  154. package/lib/typescript/src/createNavigatorFactory.d.ts +23 -5
  155. package/lib/typescript/src/createNavigatorFactory.d.ts.map +1 -1
  156. package/lib/typescript/src/findFocusedRoute.d.ts +3 -8
  157. package/lib/typescript/src/findFocusedRoute.d.ts.map +1 -1
  158. package/lib/typescript/src/getActionFromState.d.ts +4 -4
  159. package/lib/typescript/src/getActionFromState.d.ts.map +1 -1
  160. package/lib/typescript/src/getPathFromState.d.ts +3 -3
  161. package/lib/typescript/src/getPathFromState.d.ts.map +1 -1
  162. package/lib/typescript/src/getPatternParts.d.ts +5 -2
  163. package/lib/typescript/src/getPatternParts.d.ts.map +1 -1
  164. package/lib/typescript/src/getStateFromPath.d.ts +4 -7
  165. package/lib/typescript/src/getStateFromPath.d.ts.map +1 -1
  166. package/lib/typescript/src/getStateFromRouteParams.d.ts +3 -0
  167. package/lib/typescript/src/getStateFromRouteParams.d.ts.map +1 -0
  168. package/lib/typescript/src/index.d.ts +36 -33
  169. package/lib/typescript/src/index.d.ts.map +1 -1
  170. package/lib/typescript/src/internal.d.ts +3 -0
  171. package/lib/typescript/src/internal.d.ts.map +1 -0
  172. package/lib/typescript/src/isRecordEqual.d.ts.map +1 -1
  173. package/lib/typescript/src/queryString.d.ts +5 -0
  174. package/lib/typescript/src/queryString.d.ts.map +1 -0
  175. package/lib/typescript/src/theming/ThemeContext.d.ts +1 -1
  176. package/lib/typescript/src/theming/ThemeContext.d.ts.map +1 -1
  177. package/lib/typescript/src/theming/ThemeProvider.d.ts +2 -2
  178. package/lib/typescript/src/theming/ThemeProvider.d.ts.map +1 -1
  179. package/lib/typescript/src/theming/useTheme.d.ts +1 -1
  180. package/lib/typescript/src/theming/useTheme.d.ts.map +1 -1
  181. package/lib/typescript/src/types.d.ts +265 -167
  182. package/lib/typescript/src/types.d.ts.map +1 -1
  183. package/lib/typescript/src/useChildListeners.d.ts +3 -3
  184. package/lib/typescript/src/useChildListeners.d.ts.map +1 -1
  185. package/lib/typescript/src/useDescriptors.d.ts +13 -11
  186. package/lib/typescript/src/useDescriptors.d.ts.map +1 -1
  187. package/lib/typescript/src/useEventEmitter.d.ts +2 -2
  188. package/lib/typescript/src/useEventEmitter.d.ts.map +1 -1
  189. package/lib/typescript/src/useFocusEffect.d.ts.map +1 -1
  190. package/lib/typescript/src/useFocusEvents.d.ts +2 -2
  191. package/lib/typescript/src/useFocusEvents.d.ts.map +1 -1
  192. package/lib/typescript/src/useFocusedListenersChildrenAdapter.d.ts +2 -2
  193. package/lib/typescript/src/useFocusedListenersChildrenAdapter.d.ts.map +1 -1
  194. package/lib/typescript/src/useIsFocused.d.ts +4 -0
  195. package/lib/typescript/src/useIsFocused.d.ts.map +1 -1
  196. package/lib/typescript/src/useKeyedChildListeners.d.ts +4 -3
  197. package/lib/typescript/src/useKeyedChildListeners.d.ts.map +1 -1
  198. package/lib/typescript/src/useLazyValue.d.ts.map +1 -1
  199. package/lib/typescript/src/useNavigation.d.ts +4 -2
  200. package/lib/typescript/src/useNavigation.d.ts.map +1 -1
  201. package/lib/typescript/src/useNavigationBuilder.d.ts +22 -63
  202. package/lib/typescript/src/useNavigationBuilder.d.ts.map +1 -1
  203. package/lib/typescript/src/useNavigationCache.d.ts +6 -5
  204. package/lib/typescript/src/useNavigationCache.d.ts.map +1 -1
  205. package/lib/typescript/src/useNavigationContainerRef.d.ts +1 -1
  206. package/lib/typescript/src/useNavigationContainerRef.d.ts.map +1 -1
  207. package/lib/typescript/src/useNavigationHelpers.d.ts +18 -58
  208. package/lib/typescript/src/useNavigationHelpers.d.ts.map +1 -1
  209. package/lib/typescript/src/useNavigationState.d.ts +19 -3
  210. package/lib/typescript/src/useNavigationState.d.ts.map +1 -1
  211. package/lib/typescript/src/useOnAction.d.ts +9 -9
  212. package/lib/typescript/src/useOnAction.d.ts.map +1 -1
  213. package/lib/typescript/src/useOnGetNavigation.d.ts +12 -0
  214. package/lib/typescript/src/useOnGetNavigation.d.ts.map +1 -0
  215. package/lib/typescript/src/useOnGetState.d.ts +1 -1
  216. package/lib/typescript/src/useOnGetState.d.ts.map +1 -1
  217. package/lib/typescript/src/useOnPreventRemove.d.ts +5 -9
  218. package/lib/typescript/src/useOnPreventRemove.d.ts.map +1 -1
  219. package/lib/typescript/src/useOnRouteFocus.d.ts +6 -6
  220. package/lib/typescript/src/useOnRouteFocus.d.ts.map +1 -1
  221. package/lib/typescript/src/useOptionsGetters.d.ts +4 -4
  222. package/lib/typescript/src/useOptionsGetters.d.ts.map +1 -1
  223. package/lib/typescript/src/usePreventRemove.d.ts.map +1 -1
  224. package/lib/typescript/src/usePreventRemoveContext.d.ts +2 -1
  225. package/lib/typescript/src/usePreventRemoveContext.d.ts.map +1 -1
  226. package/lib/typescript/src/useRegisterNavigator.d.ts.map +1 -1
  227. package/lib/typescript/src/useRoute.d.ts +7 -3
  228. package/lib/typescript/src/useRoute.d.ts.map +1 -1
  229. package/lib/typescript/src/useRouteCache.d.ts +7 -12
  230. package/lib/typescript/src/useRouteCache.d.ts.map +1 -1
  231. package/lib/typescript/src/useScheduleUpdate.d.ts +5 -4
  232. package/lib/typescript/src/useScheduleUpdate.d.ts.map +1 -1
  233. package/lib/typescript/src/useStateForPath.d.ts +1 -1
  234. package/lib/typescript/src/useStateForPath.d.ts.map +1 -1
  235. package/lib/typescript/src/useSyncState.d.ts +1 -0
  236. package/lib/typescript/src/useSyncState.d.ts.map +1 -1
  237. package/lib/typescript/src/utilities.d.ts +79 -39
  238. package/lib/typescript/src/utilities.d.ts.map +1 -1
  239. package/lib/typescript/src/validatePathConfig.d.ts.map +1 -1
  240. package/package.json +26 -22
  241. package/src/BaseNavigationContainer.tsx +459 -332
  242. package/src/ConsumedParamsContext.tsx +5 -0
  243. package/src/DataLoading.tsx +206 -0
  244. package/src/NavigationBuilderContext.tsx +44 -25
  245. package/src/NavigationFocusedRouteStateContext.tsx +4 -4
  246. package/src/NavigationIndependentTree.tsx +60 -12
  247. package/src/NavigationProvider.tsx +59 -21
  248. package/src/NavigationRootContext.tsx +11 -0
  249. package/src/NavigationStateContext.tsx +6 -6
  250. package/src/PreventRemoveContext.tsx +1 -0
  251. package/src/PreventRemoveProvider.tsx +55 -56
  252. package/src/SceneView.tsx +31 -57
  253. package/src/StaticNavigation.tsx +715 -328
  254. package/src/StaticTreeContext.tsx +7 -0
  255. package/src/createNavigationContainerRef.tsx +12 -11
  256. package/src/createNavigatorFactory.tsx +92 -8
  257. package/src/findFocusedRoute.tsx +16 -16
  258. package/src/getActionFromState.tsx +87 -51
  259. package/src/getPathFromState.tsx +310 -134
  260. package/src/getPatternParts.tsx +98 -16
  261. package/src/getStateFromPath.tsx +971 -344
  262. package/src/getStateFromRouteParams.tsx +59 -0
  263. package/src/index.tsx +57 -6
  264. package/src/internal.tsx +2 -0
  265. package/src/isRecordEqual.tsx +3 -1
  266. package/src/queryString.ts +109 -0
  267. package/src/theming/useTheme.tsx +1 -1
  268. package/src/types.tsx +799 -377
  269. package/src/useDescriptors.tsx +151 -43
  270. package/src/useEventEmitter.tsx +53 -39
  271. package/src/useFocusEffect.tsx +5 -2
  272. package/src/useFocusEvents.tsx +19 -8
  273. package/src/useFocusedListenersChildrenAdapter.tsx +3 -3
  274. package/src/useIsFocused.tsx +18 -21
  275. package/src/useKeyedChildListeners.tsx +5 -2
  276. package/src/useLazyValue.tsx +2 -2
  277. package/src/useNavigation.tsx +9 -26
  278. package/src/useNavigationBuilder.tsx +370 -201
  279. package/src/useNavigationCache.tsx +148 -123
  280. package/src/useNavigationHelpers.tsx +27 -37
  281. package/src/useNavigationIndependentTree.tsx +1 -1
  282. package/src/useNavigationState.tsx +207 -69
  283. package/src/useOnAction.tsx +92 -18
  284. package/src/useOnGetNavigation.tsx +30 -0
  285. package/src/useOnGetState.tsx +8 -4
  286. package/src/useOnPreventRemove.tsx +78 -43
  287. package/src/useOnRouteFocus.tsx +11 -15
  288. package/src/useOptionsGetters.tsx +44 -30
  289. package/src/usePreventRemove.tsx +13 -5
  290. package/src/usePreventRemoveContext.tsx +2 -2
  291. package/src/useRegisterNavigator.tsx +4 -4
  292. package/src/useRoute.tsx +20 -6
  293. package/src/useRouteCache.tsx +57 -35
  294. package/src/useScheduleUpdate.tsx +13 -11
  295. package/src/useStateForPath.tsx +1 -1
  296. package/src/useSyncState.tsx +39 -15
  297. package/src/utilities.tsx +151 -35
  298. package/src/validatePathConfig.tsx +86 -57
  299. package/lib/module/CurrentRenderContext.js +0 -10
  300. package/lib/module/CurrentRenderContext.js.map +0 -1
  301. package/lib/module/NavigationMetaContext.js +0 -15
  302. package/lib/module/NavigationMetaContext.js.map +0 -1
  303. package/lib/module/checkSerializable.js +0 -46
  304. package/lib/module/checkSerializable.js.map +0 -1
  305. package/lib/module/useComponent.js +0 -34
  306. package/lib/module/useComponent.js.map +0 -1
  307. package/lib/module/useCurrentRender.js +0 -19
  308. package/lib/module/useCurrentRender.js.map +0 -1
  309. package/lib/typescript/src/CurrentRenderContext.d.ts +0 -9
  310. package/lib/typescript/src/CurrentRenderContext.d.ts.map +0 -1
  311. package/lib/typescript/src/NavigationMetaContext.d.ts +0 -12
  312. package/lib/typescript/src/NavigationMetaContext.d.ts.map +0 -1
  313. package/lib/typescript/src/checkSerializable.d.ts +0 -10
  314. package/lib/typescript/src/checkSerializable.d.ts.map +0 -1
  315. package/lib/typescript/src/useComponent.d.ts +0 -7
  316. package/lib/typescript/src/useComponent.d.ts.map +0 -1
  317. package/lib/typescript/src/useCurrentRender.d.ts +0 -14
  318. package/lib/typescript/src/useCurrentRender.d.ts.map +0 -1
  319. package/src/CurrentRenderContext.tsx +0 -9
  320. package/src/NavigationMetaContext.tsx +0 -14
  321. package/src/checkSerializable.tsx +0 -74
  322. package/src/useComponent.tsx +0 -37
  323. package/src/useCurrentRender.tsx +0 -31
@@ -1,33 +1,52 @@
1
- import type {
2
- InitialState,
3
- NavigationState,
4
- PartialState,
5
- } from '@react-navigation/routers';
6
- import escape from 'escape-string-regexp';
7
- import * as queryString from 'query-string';
1
+ import type { NavigationState, PartialState } from '@react-navigation/routers';
8
2
 
9
3
  import { arrayStartsWith } from './arrayStartsWith';
10
- import { findFocusedRoute } from './findFocusedRoute';
11
- import { getPatternParts, type PatternPart } from './getPatternParts';
4
+ import {
5
+ combinePatternParts,
6
+ getPatternParts,
7
+ type PatternPartRepeat,
8
+ } from './getPatternParts';
12
9
  import { isArrayEqual } from './isArrayEqual';
10
+ import * as queryString from './queryString';
13
11
  import type { PathConfig, PathConfigMap } from './types';
12
+ import type {
13
+ StandardSchemaV1,
14
+ StandardSchemaValidationResult,
15
+ } from './utilities';
14
16
  import { validatePathConfig } from './validatePathConfig';
15
17
 
16
18
  type Options<ParamList extends {}> = {
17
- path?: string;
18
- initialRouteName?: string;
19
+ path?: string | undefined;
20
+ initialRouteName?: string | undefined;
19
21
  screens: PathConfigMap<ParamList>;
20
22
  };
21
23
 
22
- type ParseConfig = Record<string, ((value: string) => unknown) | undefined>;
24
+ type ParseConfigValue =
25
+ | ((value: string) => unknown)
26
+ | StandardSchemaV1<unknown, unknown>;
27
+
28
+ type ParseConfig = Record<string, ParseConfigValue | undefined>;
23
29
 
24
30
  type RouteConfig = {
25
31
  screen: string;
26
- regex?: RegExp;
32
+ order: number;
33
+ regex?: RegExp | undefined;
34
+ pattern: string;
27
35
  segments: string[];
28
- params: { screen: string; name?: string; index: number }[];
36
+ segmentRanks: number[];
37
+ params: {
38
+ screen: string;
39
+ name: string;
40
+ index: number;
41
+ regex?: RegExp | undefined;
42
+ repeat?: PatternPartRepeat;
43
+ }[];
29
44
  routeNames: string[];
30
- parse?: ParseConfig;
45
+ parse?: ParseConfig | undefined;
46
+ explicitParamNames?: Set<string> | undefined;
47
+ pathParamNames: Set<string>;
48
+ hasNestedScreens: boolean;
49
+ shared: boolean;
31
50
  };
32
51
 
33
52
  type InitialRouteConfig = {
@@ -35,19 +54,144 @@ type InitialRouteConfig = {
35
54
  parentScreens: string[];
36
55
  };
37
56
 
38
- type ResultState = PartialState<NavigationState> & {
39
- state?: ResultState;
40
- };
41
-
42
57
  type ParsedRoute = {
43
58
  name: string;
44
- path?: string;
45
59
  params?: Record<string, unknown> | undefined;
60
+ path?: string | undefined;
46
61
  };
47
62
 
63
+ type RoutePatternPart =
64
+ | {
65
+ type: 'static';
66
+ segment: string;
67
+ rank: number;
68
+ pattern: string;
69
+ }
70
+ | {
71
+ type: 'param';
72
+ screen: string;
73
+ segment: string;
74
+ rank: number;
75
+ name: string;
76
+ pattern?: string;
77
+ regex?: RegExp;
78
+ optional?: boolean;
79
+ repeat?: PatternPartRepeat;
80
+ }
81
+ | {
82
+ type: 'wildcard';
83
+ segment: '*';
84
+ rank: number;
85
+ };
86
+
48
87
  type ConfigResources = {
49
88
  initialRoutes: InitialRouteConfig[];
50
89
  configs: RouteConfig[];
90
+ configsWithZeroOrMore: RouteConfig[];
91
+ configsByScreen: Record<string, RouteConfig[]>;
92
+ configsByPattern: Record<string, RouteConfig[]>;
93
+ prefixRegex?: RegExp | undefined;
94
+ };
95
+
96
+ const INVALID_SCHEMA_RESULT_ERROR =
97
+ 'Invalid validation result from schema. It should be an object with either "value" or "issues" property and cannot be asynchronous.';
98
+
99
+ const INVALID_PARSER_ERROR =
100
+ 'Invalid parser. Expected a function or a Standard Schema V1 object.';
101
+
102
+ const PARAM_GROUP_PREFIX = 'param_';
103
+
104
+ const NESTED_SCREEN_PARAM_NAMES = [
105
+ 'screen',
106
+ 'params',
107
+ 'initial',
108
+ 'path',
109
+ 'merge',
110
+ 'pop',
111
+ ];
112
+
113
+ const REGEX_SYNTAX_CHAR = /[|\\{}()[\]^$+*?.]/;
114
+ const HEX_DIGITS = '0123456789ABCDEF';
115
+
116
+ const getStaticSegmentPattern = (segment: string) => {
117
+ let pattern = '';
118
+
119
+ for (const char of segment) {
120
+ const code = char.charCodeAt(0);
121
+
122
+ let escaped = char;
123
+
124
+ // A hyphen uses a hexadecimal escape because a regular backslash escape
125
+ // isn't valid outside character classes when a regex uses Unicode mode.
126
+ if (char === '-') {
127
+ escaped = '\\x2d';
128
+ } else {
129
+ // Other characters with special meaning in a regex can use a regular
130
+ // backslash escape so they are matched as literal characters.
131
+ if (REGEX_SYNTAX_CHAR.test(char)) {
132
+ escaped = `\\${char}`;
133
+ }
134
+ }
135
+
136
+ // ASCII characters have a single-byte percent-encoded form that can be
137
+ // built directly. Other characters need encodeURIComponent to encode all
138
+ // bytes in their UTF-8 representation.
139
+ const percentEncoded =
140
+ char.length === 1 && code < 128
141
+ ? `%${HEX_DIGITS.charAt(code >> 4)}${HEX_DIGITS.charAt(code & 15)}`
142
+ : encodeURIComponent(char);
143
+
144
+ pattern += `(?:${escaped}|${percentEncoded})`;
145
+ }
146
+
147
+ return pattern;
148
+ };
149
+
150
+ const getExplicitParamNames = (parse?: ParseConfig) => {
151
+ const names: string[] = [];
152
+
153
+ if (parse) {
154
+ for (const name in parse) {
155
+ if (parse[name] != null) {
156
+ names.push(name);
157
+ }
158
+ }
159
+ }
160
+
161
+ return names.length ? new Set(names) : undefined;
162
+ };
163
+
164
+ const getStandardSchema = (parser: ParseConfigValue) => {
165
+ if (
166
+ '~standard' in parser &&
167
+ typeof parser['~standard'] === 'object' &&
168
+ parser['~standard'] !== null &&
169
+ 'version' in parser['~standard'] &&
170
+ parser['~standard'].version === 1 &&
171
+ 'validate' in parser['~standard'] &&
172
+ typeof parser['~standard'].validate === 'function'
173
+ ) {
174
+ return parser['~standard'];
175
+ }
176
+
177
+ return undefined;
178
+ };
179
+
180
+ const getValidationResult = (
181
+ schema: StandardSchemaV1<unknown, unknown>['~standard'],
182
+ value: unknown
183
+ ): StandardSchemaValidationResult<unknown> => {
184
+ const result = schema.validate(value);
185
+
186
+ if (
187
+ result != null &&
188
+ typeof result === 'object' &&
189
+ ('value' in result || ('issues' in result && Array.isArray(result.issues)))
190
+ ) {
191
+ return result;
192
+ }
193
+
194
+ throw new Error(INVALID_SCHEMA_RESULT_ERROR);
51
195
  };
52
196
 
53
197
  /**
@@ -73,44 +217,68 @@ type ConfigResources = {
73
217
  */
74
218
  export function getStateFromPath<ParamList extends {}>(
75
219
  path: string,
76
- options?: Options<ParamList>
77
- ): ResultState | undefined {
78
- const { initialRoutes, configs } = getConfigResources(options);
220
+ options?: Options<ParamList>,
221
+ previous?: NavigationState
222
+ ): PartialState<NavigationState> | undefined {
223
+ const {
224
+ initialRoutes,
225
+ configs,
226
+ configsWithZeroOrMore,
227
+ configsByScreen,
228
+ configsByPattern,
229
+ prefixRegex,
230
+ } = getConfigResources(options);
79
231
 
80
232
  const screens = options?.screens;
81
233
 
234
+ // When a matched config is shared, the same pattern can resolve to multiple branches
235
+ // pick the one that best matches the current navigation state
236
+ const resolveSharedConfig = (config: RouteConfig) => {
237
+ if (config.shared) {
238
+ const candidates = configsByPattern[config.pattern];
239
+
240
+ if (candidates) {
241
+ return selectSharedConfig(config, candidates, previous, initialRoutes);
242
+ }
243
+ }
244
+
245
+ return config;
246
+ };
247
+
82
248
  let remaining = path
83
249
  .replace(/\/+/g, '/') // Replace multiple slash (//) with single ones
84
250
  .replace(/^\//, '') // Remove extra leading slash
85
- .replace(/\?.*$/, ''); // Remove query params which we will handle later
251
+ .replace(/\?.*$/, '') // Remove query params which we will handle later
252
+ .replace(/%[0-9a-f]{2}/gi, (m) => m.toUpperCase()); // Normalize percent-encoding as hex digits are case-insensitive
86
253
 
87
254
  // Make sure there is a trailing slash
88
255
  remaining = remaining.endsWith('/') ? remaining : `${remaining}/`;
89
256
 
90
- const prefix = options?.path?.replace(/^\//, ''); // Remove extra leading slash
257
+ if (prefixRegex) {
258
+ const prefixMatch = remaining.match(prefixRegex);
91
259
 
92
- if (prefix) {
93
- // Make sure there is a trailing slash
94
- const normalizedPrefix = prefix.endsWith('/') ? prefix : `${prefix}/`;
95
-
96
- // If the path doesn't start with the prefix, it's not a match
97
- if (!remaining.startsWith(normalizedPrefix)) {
260
+ if (prefixMatch == null) {
98
261
  return undefined;
99
262
  }
100
263
 
101
- // Remove the prefix from the path
102
- remaining = remaining.replace(normalizedPrefix, '');
264
+ remaining = remaining.slice(prefixMatch[0].length);
103
265
  }
104
266
 
105
267
  if (screens === undefined) {
106
268
  // When no config is specified, use the path segments as route names
107
- const routes = remaining
108
- .split('/')
109
- .filter(Boolean)
110
- .map((segment) => {
111
- const name = decodeURIComponent(segment);
112
- return { name };
113
- });
269
+ const routes: ParsedRoute[] = [];
270
+
271
+ for (const segment of remaining.split('/')) {
272
+ if (!segment) {
273
+ continue;
274
+ }
275
+
276
+ try {
277
+ routes.push({ name: decodeURIComponent(segment) });
278
+ } catch {
279
+ return undefined;
280
+ }
281
+ }
114
282
 
115
283
  if (routes.length) {
116
284
  return createNestedStateObject(path, routes, initialRoutes);
@@ -122,41 +290,143 @@ export function getStateFromPath<ParamList extends {}>(
122
290
  if (remaining === '/') {
123
291
  // We need to add special handling of empty path so navigation to empty path also works
124
292
  // When handling empty path, we should only look at the root level config
125
- const match = configs.find((config) => config.segments.join('/') === '');
293
+ const match = configs.find((config) => config.pattern === '');
126
294
 
127
295
  if (match) {
128
- return createNestedStateObject(
296
+ const config = resolveSharedConfig(match);
297
+
298
+ const state = createNestedStateObject(
129
299
  path,
130
- match.routeNames.map((name) => ({ name })),
300
+ config.routeNames.map((name) => ({ name })),
131
301
  initialRoutes,
132
- configs
302
+ config
133
303
  );
304
+
305
+ if (state) {
306
+ return state;
307
+ }
308
+ }
309
+
310
+ for (const config of configsWithZeroOrMore) {
311
+ const routes = matchAgainstConfig('', config, configsByScreen);
312
+
313
+ if (routes) {
314
+ let selectedConfig = resolveSharedConfig(config);
315
+ let selectedRoutes = routes;
316
+
317
+ if (selectedConfig !== config) {
318
+ const rematchedRoutes = matchAgainstConfig(
319
+ '',
320
+ selectedConfig,
321
+ configsByScreen
322
+ );
323
+
324
+ if (rematchedRoutes === undefined) {
325
+ selectedConfig = config;
326
+ } else {
327
+ selectedRoutes = rematchedRoutes;
328
+ }
329
+ }
330
+
331
+ const state = createNestedStateObject(
332
+ path,
333
+ selectedRoutes,
334
+ initialRoutes,
335
+ selectedConfig
336
+ );
337
+
338
+ if (state) {
339
+ return state;
340
+ }
341
+ }
134
342
  }
135
343
 
136
344
  return undefined;
137
345
  }
138
346
 
139
- let result: PartialState<NavigationState> | undefined;
140
- let current: PartialState<NavigationState> | undefined;
141
-
142
347
  // We match the whole path against the regex instead of segments
143
348
  // This makes sure matches such as wildcard will catch any unmatched routes, even if nested
144
- const { routes, remainingPath } = matchAgainstConfigs(remaining, configs);
349
+ const firstRawSegment = remaining.split('/')[0];
350
+
351
+ let firstDecodedSegment: string | undefined;
145
352
 
146
- if (routes !== undefined) {
147
- // This will always be empty if full path matched
148
- current = createNestedStateObject(path, routes, initialRoutes, configs);
149
- remaining = remainingPath;
150
- result = current;
353
+ try {
354
+ firstDecodedSegment =
355
+ firstRawSegment != null ? decodeURIComponent(firstRawSegment) : undefined;
356
+ } catch {
357
+ firstDecodedSegment = undefined;
151
358
  }
152
359
 
153
- if (current == null || result == null) {
154
- return undefined;
360
+ for (const config of configs) {
361
+ // Regex matching is expensive, so skip configs that can't match this path.
362
+ if (
363
+ !canMatchFirstSegment(
364
+ config.segments[0],
365
+ firstDecodedSegment,
366
+ firstRawSegment
367
+ )
368
+ ) {
369
+ continue;
370
+ }
371
+
372
+ const routes = matchAgainstConfig(remaining, config, configsByScreen);
373
+
374
+ if (routes === undefined) {
375
+ continue;
376
+ }
377
+
378
+ let selectedConfig = resolveSharedConfig(config);
379
+ let selectedRoutes = routes;
380
+
381
+ if (selectedConfig !== config) {
382
+ const rematchedRoutes = matchAgainstConfig(
383
+ remaining,
384
+ selectedConfig,
385
+ configsByScreen
386
+ );
387
+
388
+ if (rematchedRoutes === undefined) {
389
+ selectedConfig = config;
390
+ } else {
391
+ selectedRoutes = rematchedRoutes;
392
+ }
393
+ }
394
+
395
+ const state = createNestedStateObject(
396
+ path,
397
+ selectedRoutes,
398
+ initialRoutes,
399
+ selectedConfig
400
+ );
401
+
402
+ if (state !== undefined) {
403
+ return state;
404
+ }
155
405
  }
156
406
 
157
- return result;
407
+ return undefined;
158
408
  }
159
409
 
410
+ const canMatchFirstSegment = (
411
+ configSegment: string | undefined,
412
+ decodedSegment: string | undefined,
413
+ rawSegment: string | undefined
414
+ ) => {
415
+ if (decodedSegment === undefined) {
416
+ return true;
417
+ }
418
+
419
+ if (configSegment === undefined) {
420
+ return false;
421
+ }
422
+
423
+ if (configSegment === '*' || configSegment.startsWith(':')) {
424
+ return true;
425
+ }
426
+
427
+ return configSegment === decodedSegment || configSegment === rawSegment;
428
+ };
429
+
160
430
  /**
161
431
  * Reference to the last used config resources. This is used to avoid recomputing the config resources when the options are the same.
162
432
  */
@@ -165,11 +435,17 @@ const cachedConfigResources = new WeakMap<Options<{}>, ConfigResources>();
165
435
  function getConfigResources<ParamList extends {}>(
166
436
  options: Options<ParamList> | undefined
167
437
  ) {
168
- if (!options) return prepareConfigResources();
438
+ if (!options) {
439
+ return prepareConfigResources();
440
+ }
169
441
 
170
442
  const cached = cachedConfigResources.get(options);
171
443
 
172
- if (cached) return cached;
444
+ if (cached) {
445
+ return cached;
446
+ }
447
+
448
+ validatePathConfig(options);
173
449
 
174
450
  const resources = prepareConfigResources(options);
175
451
 
@@ -179,21 +455,50 @@ function getConfigResources<ParamList extends {}>(
179
455
  }
180
456
 
181
457
  function prepareConfigResources(options?: Options<{}>) {
182
- if (options) {
183
- validatePathConfig(options);
184
- }
185
-
186
458
  const initialRoutes = getInitialRoutes(options);
187
459
  const configs = getSortedNormalizedConfigs(initialRoutes, options?.screens);
460
+ const prefix = options?.path?.replace(/^\//, '');
188
461
 
189
- checkForDuplicatedConfigs(configs);
462
+ let prefixRegex: RegExp | undefined;
190
463
 
191
- const configWithRegexes = getConfigsWithRegexes(configs);
464
+ if (prefix) {
465
+ const normalizedPrefix = prefix.endsWith('/') ? prefix : `${prefix}/`;
466
+ const prefixPattern = normalizedPrefix
467
+ .split('/')
468
+ .map(getStaticSegmentPattern)
469
+ .join('/');
470
+
471
+ prefixRegex = new RegExp(`^${prefixPattern}`);
472
+ }
473
+
474
+ const configsByScreen: Record<string, RouteConfig[]> = {};
475
+ const configsByPattern: Record<string, RouteConfig[]> = {};
476
+ const configsWithZeroOrMore: RouteConfig[] = [];
477
+
478
+ for (const c of configs) {
479
+ if (c.params.some((param) => param.repeat === 'zero-or-more')) {
480
+ configsWithZeroOrMore.push(c);
481
+ }
482
+
483
+ (configsByScreen[c.screen] ??= []).push(c);
484
+
485
+ const existing = configsByPattern[c.pattern];
486
+
487
+ if (existing) {
488
+ checkForDuplicatedConfigs(existing[existing.length - 1], c);
489
+ existing.push(c);
490
+ } else {
491
+ configsByPattern[c.pattern] = [c];
492
+ }
493
+ }
192
494
 
193
495
  return {
194
496
  initialRoutes,
195
497
  configs,
196
- configWithRegexes,
498
+ configsWithZeroOrMore,
499
+ configsByScreen,
500
+ configsByPattern,
501
+ prefixRegex,
197
502
  };
198
503
  }
199
504
 
@@ -215,216 +520,304 @@ function getSortedNormalizedConfigs(
215
520
  screens: Record<string, string | PathConfig<{}>> = {}
216
521
  ) {
217
522
  // Create a normalized configs array which will be easier to use
218
- return ([] as RouteConfig[])
219
- .concat(
220
- ...Object.keys(screens).map((key) =>
221
- createNormalizedConfigs(key, screens, initialRoutes, [], [], [])
222
- )
223
- )
224
- .sort((a, b) => {
225
- // Sort config from most specific to least specific:
226
- // - more segments
227
- // - static segments
228
- // - params with regex
229
- // - regular params
230
- // - wildcard
231
-
232
- // If 2 patterns are same, move the one with less route names up
233
- // This is an error state, so it's only useful for consistent error messages
234
- if (isArrayEqual(a.segments, b.segments)) {
235
- return b.routeNames.join('>').localeCompare(a.routeNames.join('>'));
236
- }
523
+ const configs: RouteConfig[] = [];
524
+
525
+ for (const key in screens) {
526
+ setupNormalizedConfigs(key, screens, initialRoutes, configs, [], [], []);
527
+ }
528
+
529
+ for (let order = 0; order < configs.length; order++) {
530
+ const config = configs[order];
531
+
532
+ if (config) {
533
+ config.order = order;
534
+ }
535
+ }
237
536
 
238
- // If one of the patterns starts with the other, it's more exhaustive
239
- // So move it up
240
- if (arrayStartsWith(a.segments, b.segments)) {
537
+ return configs.sort((a, b) => {
538
+ // Sort config from most specific to least specific:
539
+ // - static segments
540
+ // - params with regex
541
+ // - regular params
542
+ // - one-or-more params
543
+ // - the end of the pattern
544
+ // - zero-or-more params
545
+ // - wildcard
546
+
547
+ // If 2 patterns are same, move the one with less route names up
548
+ // This is an error state, so it's only useful for consistent error messages
549
+ if (a.pattern === b.pattern && isArrayEqual(a.segments, b.segments)) {
550
+ if (
551
+ a.routeNames.length > b.routeNames.length &&
552
+ arrayStartsWith(a.routeNames, b.routeNames)
553
+ ) {
241
554
  return -1;
242
555
  }
243
556
 
244
- if (arrayStartsWith(b.segments, a.segments)) {
557
+ if (
558
+ b.routeNames.length > a.routeNames.length &&
559
+ arrayStartsWith(b.routeNames, a.routeNames)
560
+ ) {
245
561
  return 1;
246
562
  }
247
563
 
248
- for (let i = 0; i < Math.max(a.segments.length, b.segments.length); i++) {
249
- // if b is longer, b gets higher priority
250
- if (a.segments[i] == null) {
251
- return 1;
252
- }
564
+ return a.routeNames.length - b.routeNames.length || a.order - b.order;
565
+ }
253
566
 
254
- // if a is longer, a gets higher priority
255
- if (b.segments[i] == null) {
256
- return -1;
257
- }
567
+ for (let i = 0; i < Math.max(a.segments.length, b.segments.length); i++) {
568
+ const rank = (a.segmentRanks[i] ?? 5) - (b.segmentRanks[i] ?? 5);
569
+
570
+ if (rank !== 0) {
571
+ return rank;
572
+ }
573
+ }
574
+
575
+ return a.order - b.order;
576
+ });
577
+ }
578
+
579
+ const getSegmentRank = (segment: string | undefined) => {
580
+ if (segment === undefined) {
581
+ return 5;
582
+ }
583
+
584
+ if (segment === '*') {
585
+ return 8;
586
+ }
587
+
588
+ if (!segment.startsWith(':')) {
589
+ return 0;
590
+ }
591
+
592
+ const regex = segment.includes('(');
593
+
594
+ if (segment.endsWith('+')) {
595
+ return regex ? 3 : 4;
596
+ }
597
+
598
+ if (segment.endsWith('*')) {
599
+ return regex ? 6 : 7;
600
+ }
601
+
602
+ return regex ? 1 : 2;
603
+ };
604
+
605
+ // Throw if two configs resolve to the same pattern but conflicting screens
606
+ function checkForDuplicatedConfigs(
607
+ existing: RouteConfig | undefined,
608
+ config: RouteConfig
609
+ ) {
610
+ if (!existing) {
611
+ return;
612
+ }
613
+
614
+ const a = existing.routeNames;
615
+ const b = config.routeNames;
616
+
617
+ // It's not a problem if the path string omitted from an innermost screen
618
+ // For example, it's ok if a path resolves to `A > B > C` or `A > B`
619
+ const intersects =
620
+ a.length > b.length ? arrayStartsWith(a, b) : arrayStartsWith(b, a);
621
+
622
+ if (!intersects && (!existing.shared || !config.shared)) {
623
+ throw new Error(
624
+ `Found conflicting screens with the same pattern. The pattern '${
625
+ config.pattern
626
+ }' resolves to both '${a.join(' > ')}' and '${b.join(
627
+ ' > '
628
+ )}'. Patterns must be unique and cannot resolve to more than one screen unless shared: true is specified.`
629
+ );
630
+ }
631
+ }
632
+
633
+ const matchAgainstConfig = (
634
+ remaining: string,
635
+ config: RouteConfig,
636
+ configsByScreen: Record<string, RouteConfig[]>
637
+ ) => {
638
+ if (!config.regex) {
639
+ return undefined;
640
+ }
641
+
642
+ const match = remaining.match(config.regex);
643
+
644
+ if (!match) {
645
+ return undefined;
646
+ }
258
647
 
259
- const aWildCard = a.segments[i] === '*';
260
- const bWildCard = b.segments[i] === '*';
261
- const aParam = a.segments[i].startsWith(':');
262
- const bParam = b.segments[i].startsWith(':');
263
- const aRegex = aParam && a.segments[i].includes('(');
264
- const bRegex = bParam && b.segments[i].includes('(');
648
+ let validationFailed = false;
649
+ const matchedRoutes: ParsedRoute[] = [];
265
650
 
266
- // if both are wildcard or regex, we compare next component
267
- if ((aWildCard && bWildCard) || (aRegex && bRegex)) {
651
+ for (const [index, routeName] of config.routeNames.entries()) {
652
+ // Check matching name, route names and pattern in case same screen is used
653
+ // at different levels or branches in config
654
+ const routeConfig = configsByScreen[routeName]?.find(
655
+ (c) =>
656
+ index === c.routeNames.length - 1 &&
657
+ arrayStartsWith(config.routeNames, c.routeNames) &&
658
+ arrayStartsWith(config.segments, c.segments)
659
+ );
660
+
661
+ let params: Record<string, unknown> | undefined;
662
+ let hasParams = false;
663
+
664
+ if (routeConfig && match.groups) {
665
+ params = {};
666
+
667
+ for (const param of routeConfig.params) {
668
+ if (param.screen !== routeName) {
268
669
  continue;
269
670
  }
270
671
 
271
- // if only a is wildcard, b gets higher priority
272
- if (aWildCard && !bWildCard) {
273
- return 1;
274
- }
672
+ let value = match.groups[`${PARAM_GROUP_PREFIX}${param.index}`];
275
673
 
276
- // if only b is wildcard, a gets higher priority
277
- if (bWildCard && !aWildCard) {
278
- return -1;
674
+ if (param.repeat && value != null) {
675
+ value = value.replace(/\/$/, '');
279
676
  }
280
677
 
281
- // If only a has a param, b gets higher priority
282
- if (aParam && !bParam) {
283
- return 1;
284
- }
678
+ let decoded: string;
679
+ let decodedSegments: string[] | undefined;
285
680
 
286
- // If only b has a param, a gets higher priority
287
- if (bParam && !aParam) {
288
- return -1;
681
+ try {
682
+ if (param.repeat) {
683
+ if (value == null) {
684
+ decodedSegments = [];
685
+ } else {
686
+ decodedSegments = value.split('/').map(decodeURIComponent);
687
+ }
688
+
689
+ decoded = decodedSegments.join('/');
690
+ } else {
691
+ if (value == null) {
692
+ params[param.name] = undefined;
693
+ hasParams = true;
694
+ continue;
695
+ }
696
+
697
+ decoded = decodeURIComponent(value);
698
+ }
699
+ } catch {
700
+ return undefined;
289
701
  }
290
702
 
291
- // if only a has regex, a gets higher priority
292
- if (aRegex && !bRegex) {
293
- return -1;
703
+ // Percent-encoded values are matched permissively
704
+ // So validate the decoded value against the custom regex
705
+ if (param.regex) {
706
+ const regex = param.regex;
707
+ const values = decodedSegments ?? [decoded];
708
+ const rawValues = param.repeat ? (value?.split('/') ?? []) : [value];
709
+
710
+ if (
711
+ values.some(
712
+ (item, index) => item !== rawValues[index] && !regex.test(item)
713
+ )
714
+ ) {
715
+ return undefined;
716
+ }
294
717
  }
295
718
 
296
- // if only b has regex, b gets higher priority
297
- if (bRegex && !aRegex) {
298
- return 1;
719
+ const parser = routeConfig.parse?.[param.name];
720
+
721
+ if (!parser) {
722
+ params[param.name] = decoded;
723
+ hasParams = true;
724
+ continue;
299
725
  }
300
- }
301
726
 
302
- return a.segments.length - b.segments.length;
303
- });
304
- }
727
+ const schema = getStandardSchema(parser);
305
728
 
306
- function checkForDuplicatedConfigs(configs: RouteConfig[]) {
307
- // Check for duplicate patterns in the config
308
- configs.reduce<Record<string, RouteConfig>>((acc, config) => {
309
- const pattern = config.segments.join('/');
310
-
311
- if (acc[pattern]) {
312
- const a = acc[pattern].routeNames;
313
- const b = config.routeNames;
314
-
315
- // It's not a problem if the path string omitted from a inner most screen
316
- // For example, it's ok if a path resolves to `A > B > C` or `A > B`
317
- const intersects =
318
- a.length > b.length
319
- ? b.every((it, i) => a[i] === it)
320
- : a.every((it, i) => b[i] === it);
321
-
322
- if (!intersects) {
323
- throw new Error(
324
- `Found conflicting screens with the same pattern. The pattern '${
325
- pattern
326
- }' resolves to both '${a.join(' > ')}' and '${b.join(
327
- ' > '
328
- )}'. Patterns must be unique and cannot resolve to more than one screen.`
329
- );
330
- }
331
- }
729
+ if (schema) {
730
+ const result = getValidationResult(schema, decoded);
332
731
 
333
- return Object.assign(acc, {
334
- [pattern]: config,
335
- });
336
- }, {});
337
- }
732
+ if (result.issues) {
733
+ validationFailed = true;
734
+ break;
735
+ }
338
736
 
339
- function getConfigsWithRegexes(configs: RouteConfig[]) {
340
- return configs.map((c) => ({
341
- ...c,
342
- // Add `$` to the regex to make sure it matches till end of the path and not just beginning
343
- regex: c.regex ? new RegExp(c.regex.source + '$') : undefined,
344
- }));
345
- }
737
+ params[param.name] = result.value;
738
+ hasParams = true;
739
+ continue;
740
+ }
346
741
 
347
- const matchAgainstConfigs = (remaining: string, configs: RouteConfig[]) => {
348
- let routes: ParsedRoute[] | undefined;
349
- let remainingPath = remaining;
742
+ if (typeof parser === 'function') {
743
+ params[param.name] = parser(decoded);
744
+ hasParams = true;
745
+ continue;
746
+ }
350
747
 
351
- // Go through all configs, and see if the next path segment matches our regex
352
- for (const config of configs) {
353
- if (!config.regex) {
354
- continue;
355
- }
748
+ throw new Error(INVALID_PARSER_ERROR);
749
+ }
356
750
 
357
- const match = remainingPath.match(config.regex);
751
+ if (validationFailed) {
752
+ // A failed param validation invalidates the whole nested route chain for this config.
753
+ break;
754
+ }
755
+ }
358
756
 
359
- // If our regex matches, we need to extract params from the path
360
- if (match) {
361
- routes = config.routeNames.map((routeName) => {
362
- const routeConfig = configs.find((c) => {
363
- // Check matching name AND pattern in case same screen is used at different levels in config
364
- return (
365
- c.screen === routeName &&
366
- arrayStartsWith(config.segments, c.segments)
367
- );
368
- });
757
+ if (params && hasParams) {
758
+ matchedRoutes.push({ name: routeName, params });
759
+ } else {
760
+ matchedRoutes.push({ name: routeName });
761
+ }
762
+ }
369
763
 
370
- const params =
371
- routeConfig && match.groups
372
- ? Object.fromEntries(
373
- Object.entries(match.groups)
374
- .map(([key, value]) => {
375
- const index = Number(key.replace('param_', ''));
376
- const param = routeConfig.params.find(
377
- (it) => it.index === index
378
- );
379
-
380
- if (param?.screen === routeName && param?.name) {
381
- return [param.name, value];
382
- }
383
-
384
- return null;
385
- })
386
- .filter((it) => it != null)
387
- .map(([key, value]) => {
388
- if (value == null) {
389
- return [key, undefined];
390
- }
391
-
392
- const decoded = decodeURIComponent(value);
393
- const parsed = routeConfig.parse?.[key]
394
- ? routeConfig.parse[key](decoded)
395
- : decoded;
396
-
397
- return [key, parsed];
398
- })
399
- )
400
- : undefined;
764
+ if (validationFailed) {
765
+ return undefined;
766
+ }
401
767
 
402
- if (params && Object.keys(params).length) {
403
- return { name: routeName, params };
404
- }
768
+ return matchedRoutes;
769
+ };
405
770
 
406
- return { name: routeName };
407
- });
771
+ const getRoutePatternParts = (
772
+ path: string,
773
+ screen: string
774
+ ): RoutePatternPart[] =>
775
+ getPatternParts(path).map((part) => {
776
+ const rank = getSegmentRank(part.segment);
408
777
 
409
- remainingPath = remainingPath.replace(match[0], '');
778
+ if (part.name) {
779
+ return {
780
+ type: 'param',
781
+ screen,
782
+ segment: part.segment,
783
+ rank,
784
+ name: part.name,
785
+ ...(part.pattern
786
+ ? {
787
+ pattern: part.pattern,
788
+ regex: new RegExp(`^(?:${part.pattern})$`),
789
+ }
790
+ : null),
791
+ ...(part.optional ? { optional: true } : null),
792
+ ...(part.repeat ? { repeat: part.repeat } : null),
793
+ };
794
+ }
410
795
 
411
- break;
796
+ if (part.segment === '*') {
797
+ return {
798
+ type: 'wildcard',
799
+ segment: part.segment,
800
+ rank,
801
+ };
412
802
  }
413
- }
414
803
 
415
- return { routes, remainingPath };
416
- };
804
+ return {
805
+ type: 'static',
806
+ segment: part.segment,
807
+ rank,
808
+ pattern: getStaticSegmentPattern(part.segment),
809
+ };
810
+ });
417
811
 
418
- const createNormalizedConfigs = (
812
+ const setupNormalizedConfigs = (
419
813
  screen: string,
420
814
  routeConfig: Record<string, string | PathConfig<{}>>,
421
815
  initials: InitialRouteConfig[],
422
- paths: { screen: string; path: string }[],
816
+ configs: RouteConfig[],
817
+ parts: RoutePatternPart[],
423
818
  parentScreens: string[],
424
819
  routeNames: string[]
425
- ): RouteConfig[] => {
426
- const configs: RouteConfig[] = [];
427
-
820
+ ): void => {
428
821
  routeNames.push(screen);
429
822
 
430
823
  parentScreens.push(screen);
@@ -432,19 +825,31 @@ const createNormalizedConfigs = (
432
825
  const config = routeConfig[screen];
433
826
 
434
827
  if (typeof config === 'string') {
435
- paths.push({ screen, path: config });
436
- configs.push(createConfigItem(screen, [...routeNames], [...paths]));
828
+ const configParts = combinePatternParts(
829
+ parts,
830
+ getRoutePatternParts(config, screen)
831
+ );
832
+
833
+ configs.push(createConfigItem(screen, [...routeNames], configParts));
437
834
  } else if (typeof config === 'object') {
438
835
  // if an object is specified as the value (e.g. Foo: { ... }),
439
836
  // it can have `path` property and
440
837
  // it could have `screens` prop which has nested configs
441
- if (typeof config.path === 'string') {
442
- if (config.exact && config.path == null) {
443
- throw new Error(
444
- `Screen '${screen}' doesn't specify a 'path'. A 'path' needs to be specified when specifying 'exact: true'. If you don't want this screen in the URL, specify it as empty string, e.g. \`path: ''\`.`
445
- );
446
- }
838
+ const nestedScreens =
839
+ 'screens' in config
840
+ ? (config.screens as Record<string, string | PathConfig<{}>>)
841
+ : undefined;
842
+ const hasNestedScreens = !!nestedScreens;
843
+ const currentParts =
844
+ typeof config.path === 'string'
845
+ ? combinePatternParts(
846
+ parts,
847
+ getRoutePatternParts(config.path, screen),
848
+ config.exact
849
+ )
850
+ : parts;
447
851
 
852
+ if (typeof config.path === 'string') {
448
853
  // We should add alias configs after the main config
449
854
  // So unless they are more specific, main config will be matched first
450
855
  const aliasConfigs = [];
@@ -456,8 +861,10 @@ const createNormalizedConfigs = (
456
861
  createConfigItem(
457
862
  screen,
458
863
  [...routeNames],
459
- [...paths, { screen, path: alias }],
460
- config.parse
864
+ combinePatternParts(parts, getRoutePatternParts(alias, screen)),
865
+ config.parse,
866
+ hasNestedScreens,
867
+ false
461
868
  )
462
869
  );
463
870
  } else if (typeof alias === 'object') {
@@ -465,25 +872,29 @@ const createNormalizedConfigs = (
465
872
  createConfigItem(
466
873
  screen,
467
874
  [...routeNames],
468
- alias.exact
469
- ? [{ screen, path: alias.path }]
470
- : [...paths, { screen, path: alias.path }],
471
- alias.parse
875
+ combinePatternParts(
876
+ parts,
877
+ getRoutePatternParts(alias.path, screen),
878
+ alias.exact
879
+ ),
880
+ alias.parse,
881
+ hasNestedScreens,
882
+ alias.shared === true
472
883
  )
473
884
  );
474
885
  }
475
886
  }
476
887
  }
477
888
 
478
- if (config.exact) {
479
- // If it's an exact path, we don't need to keep track of the parent screens
480
- // So we can clear it
481
- paths.length = 0;
482
- }
483
-
484
- paths.push({ screen, path: config.path });
485
889
  configs.push(
486
- createConfigItem(screen, [...routeNames], [...paths], config.parse)
890
+ createConfigItem(
891
+ screen,
892
+ [...routeNames],
893
+ currentParts,
894
+ config.parse,
895
+ hasNestedScreens,
896
+ config.shared === true
897
+ )
487
898
  );
488
899
 
489
900
  configs.push(...aliasConfigs);
@@ -499,7 +910,7 @@ const createNormalizedConfigs = (
499
910
  );
500
911
  }
501
912
 
502
- if ('screens' in config && config.screens) {
913
+ if (nestedScreens) {
503
914
  // property `initialRouteName` without `screens` has no purpose
504
915
  if (
505
916
  'initialRouteName' in config &&
@@ -511,89 +922,189 @@ const createNormalizedConfigs = (
511
922
  });
512
923
  }
513
924
 
514
- Object.keys(config.screens).forEach((nestedConfig) => {
515
- const result = createNormalizedConfigs(
925
+ for (const nestedConfig in nestedScreens) {
926
+ setupNormalizedConfigs(
516
927
  nestedConfig,
517
- config.screens as Record<string, string | PathConfig<{}>>,
928
+ nestedScreens,
518
929
  initials,
519
- [...paths],
930
+ configs,
931
+ currentParts,
520
932
  [...parentScreens],
521
933
  routeNames
522
934
  );
523
-
524
- configs.push(...result);
525
- });
935
+ }
526
936
  }
527
937
  }
528
938
 
529
939
  routeNames.pop();
530
-
531
- return configs;
532
940
  };
533
941
 
534
942
  const createConfigItem = (
535
943
  screen: string,
536
944
  routeNames: string[],
537
- paths: { screen: string; path: string }[],
538
- parse?: ParseConfig
945
+ parts: RoutePatternPart[],
946
+ parse?: ParseConfig,
947
+ hasNestedScreens = false,
948
+ shared = false
539
949
  ): RouteConfig => {
540
- const parts: (PatternPart & { screen: string })[] = [];
950
+ let regex: RegExp | undefined;
541
951
 
542
- // Parse the path string into parts for easier matching
543
- for (const { screen, path } of paths) {
544
- parts.push(...getPatternParts(path).map((part) => ({ ...part, screen })));
545
- }
952
+ const segments: string[] = [];
953
+ const segmentRanks: number[] = [];
954
+ const params: RouteConfig['params'] = [];
955
+ const pathParamNames = new Set<string>();
546
956
 
547
- const regex = parts.length
548
- ? new RegExp(
549
- `^(${parts
550
- .map((it, i) => {
551
- if (it.param) {
552
- const reg = it.regex || '[^/]+';
957
+ if (parts.length) {
958
+ let regexString = '^(';
959
+ let index = 0;
553
960
 
554
- return `(((?<param_${i}>${reg})\\/)${it.optional ? '?' : ''})`;
555
- }
961
+ for (const part of parts) {
962
+ segments.push(part.segment);
963
+ segmentRanks.push(part.rank);
556
964
 
557
- return `${it.segment === '*' ? '.*' : escape(it.segment)}\\/`;
558
- })
559
- .join('')})$`
560
- )
561
- : undefined;
562
-
563
- const segments = parts.map((it) => it.segment);
564
- const params = parts
565
- .map((it, i) =>
566
- it.param
567
- ? {
568
- index: i,
569
- screen: it.screen,
570
- name: it.param,
571
- }
572
- : null
573
- )
574
- .filter((it) => it != null);
965
+ if (part.type === 'param') {
966
+ // A custom regex may not match the percent-encoded form of the value
967
+ // So also accept segments containing an encoded character and validate them after decoding
968
+ let reg = '[^/]+';
969
+
970
+ if (part.pattern) {
971
+ const encodedReg = `(?=[^/]*%[0-9A-F]{2})[^/]+`;
972
+
973
+ reg = part.repeat
974
+ ? `(?:${encodedReg})|(?![^/]*%[0-9A-F]{2})(?:${part.pattern})`
975
+ : `(?:${part.pattern})|${encodedReg}`;
976
+ }
977
+
978
+ if (part.repeat) {
979
+ const repeatedReg = `(?:(?:${reg})\\/)+`;
980
+
981
+ regexString += `(?<${PARAM_GROUP_PREFIX}${index}>${repeatedReg})${
982
+ part.repeat === 'zero-or-more' ? '?' : ''
983
+ }`;
984
+ } else {
985
+ regexString += `(((?<${PARAM_GROUP_PREFIX}${index}>${reg})\\/)${part.optional ? '?' : ''})`;
986
+ }
987
+
988
+ params.push({
989
+ index,
990
+ screen: part.screen,
991
+ name: part.name,
992
+ regex: part.regex,
993
+ ...(part.repeat ? { repeat: part.repeat } : {}),
994
+ });
995
+
996
+ if (part.screen === screen) {
997
+ pathParamNames.add(part.name);
998
+ }
999
+ } else if (part.type === 'wildcard') {
1000
+ regexString += `.*\\/`;
1001
+ } else {
1002
+ regexString += `${part.pattern}\\/`;
1003
+ }
1004
+
1005
+ index++;
1006
+ }
1007
+
1008
+ regexString += ')$';
1009
+ regex = new RegExp(regexString);
1010
+ }
1011
+
1012
+ const pattern = segments.join('/');
575
1013
 
576
1014
  return {
577
1015
  screen,
1016
+ order: 0,
578
1017
  regex,
1018
+ pattern,
579
1019
  segments,
1020
+ segmentRanks,
580
1021
  params,
581
1022
  routeNames,
582
1023
  parse,
1024
+ explicitParamNames: getExplicitParamNames(parse),
1025
+ pathParamNames,
1026
+ hasNestedScreens,
1027
+ shared,
583
1028
  };
584
1029
  };
585
1030
 
586
- const findParseConfigForRoute = (
587
- routeName: string,
588
- flatConfig: RouteConfig[]
589
- ): ParseConfig | undefined => {
590
- for (const config of flatConfig) {
591
- if (routeName === config.routeNames[config.routeNames.length - 1]) {
592
- return config.parse;
1031
+ const selectSharedConfig = (
1032
+ match: RouteConfig,
1033
+ candidates: RouteConfig[],
1034
+ previous: NavigationState | PartialState<NavigationState> | undefined,
1035
+ initialRoutes: InitialRouteConfig[]
1036
+ ) => {
1037
+ // Walk the previous state down to the focused leaf to get the active route path
1038
+ const focusedRouteNames: string[] = [];
1039
+
1040
+ let current = previous;
1041
+
1042
+ while (current?.routes.length) {
1043
+ const index =
1044
+ typeof current.index === 'number'
1045
+ ? current.index
1046
+ : current.routes.length - 1;
1047
+
1048
+ const route = current.routes[index];
1049
+
1050
+ if (route == null) {
1051
+ break;
593
1052
  }
1053
+
1054
+ focusedRouteNames.push(route.name);
1055
+ current = route.state;
594
1056
  }
595
1057
 
596
- return undefined;
1058
+ // Rank a candidate by how far it aligns with the focused path, then by
1059
+ // how many of its levels match a configured initial route
1060
+ const score = (routeNames: string[]) => {
1061
+ let focused = 0;
1062
+
1063
+ while (
1064
+ routeNames[focused] !== undefined &&
1065
+ routeNames[focused] === focusedRouteNames[focused]
1066
+ ) {
1067
+ focused++;
1068
+ }
1069
+
1070
+ let initial = 0;
1071
+
1072
+ for (const config of initialRoutes) {
1073
+ if (
1074
+ arrayStartsWith(routeNames, config.parentScreens) &&
1075
+ routeNames[config.parentScreens.length] === config.initialRouteName
1076
+ ) {
1077
+ initial++;
1078
+ }
1079
+ }
1080
+
1081
+ return { focused, initial };
1082
+ };
1083
+
1084
+ let selected = match;
1085
+ let best = { focused: -1, initial: -1 };
1086
+
1087
+ for (const candidate of candidates) {
1088
+ if (!candidate.shared) {
1089
+ continue;
1090
+ }
1091
+
1092
+ const { focused, initial } = score(candidate.routeNames);
1093
+
1094
+ const better =
1095
+ focused !== best.focused
1096
+ ? focused > best.focused
1097
+ : initial !== best.initial
1098
+ ? initial > best.initial
1099
+ : candidate.order < selected.order;
1100
+
1101
+ if (better) {
1102
+ selected = candidate;
1103
+ best = { focused, initial };
1104
+ }
1105
+ }
1106
+
1107
+ return selected;
597
1108
  };
598
1109
 
599
1110
  // Try to find an initial route connected with the one passed
@@ -606,7 +1117,14 @@ const findInitialRoute = (
606
1117
  if (parentScreens.length === config.parentScreens.length) {
607
1118
  let sameParents = true;
608
1119
  for (let i = 0; i < parentScreens.length; i++) {
609
- if (parentScreens[i].localeCompare(config.parentScreens[i]) !== 0) {
1120
+ const parentScreen = parentScreens[i];
1121
+ const configParentScreen = config.parentScreens[i];
1122
+
1123
+ if (
1124
+ parentScreen == null ||
1125
+ configParentScreen == null ||
1126
+ parentScreen.localeCompare(configParentScreen) !== 0
1127
+ ) {
610
1128
  sameParents = false;
611
1129
  break;
612
1130
  }
@@ -627,7 +1145,7 @@ const createStateObject = (
627
1145
  initialRoute: string | undefined,
628
1146
  route: ParsedRoute,
629
1147
  isEmpty: boolean
630
- ): InitialState => {
1148
+ ): PartialState<NavigationState> => {
631
1149
  if (isEmpty) {
632
1150
  if (initialRoute) {
633
1151
  return {
@@ -657,75 +1175,184 @@ const createNestedStateObject = (
657
1175
  path: string,
658
1176
  routes: ParsedRoute[],
659
1177
  initialRoutes: InitialRouteConfig[],
660
- flatConfig?: RouteConfig[]
661
- ) => {
662
- let route = routes.shift() as ParsedRoute;
1178
+ routeConfig?: RouteConfig
1179
+ ): PartialState<NavigationState> | undefined => {
1180
+ const focusedRoute = routes[routes.length - 1];
1181
+
1182
+ if (focusedRoute == null) {
1183
+ return undefined;
1184
+ }
1185
+
1186
+ focusedRoute.path = path.replace(/\/$/, '');
1187
+
1188
+ const queryParams = parseQueryParams(
1189
+ path,
1190
+ routeConfig?.parse,
1191
+ routeConfig?.pathParamNames,
1192
+ routeConfig?.explicitParamNames,
1193
+ routeConfig?.hasNestedScreens,
1194
+ focusedRoute.params
1195
+ );
1196
+
1197
+ if (!queryParams.valid) {
1198
+ return undefined;
1199
+ }
1200
+
1201
+ if (queryParams.params) {
1202
+ focusedRoute.params = { ...focusedRoute.params, ...queryParams.params };
1203
+ }
1204
+
1205
+ let route = routes.shift();
1206
+
1207
+ if (route == null) {
1208
+ return undefined;
1209
+ }
1210
+
663
1211
  const parentScreens: string[] = [];
664
1212
 
665
1213
  let initialRoute = findInitialRoute(route.name, parentScreens, initialRoutes);
666
1214
 
667
1215
  parentScreens.push(route.name);
668
1216
 
669
- const state: InitialState = createStateObject(
670
- initialRoute,
671
- route,
672
- routes.length === 0
673
- );
1217
+ const state = createStateObject(initialRoute, route, routes.length === 0);
674
1218
 
675
1219
  if (routes.length > 0) {
676
1220
  let nestedState = state;
677
1221
 
678
- while ((route = routes.shift() as ParsedRoute)) {
1222
+ let nextRoute = routes.shift();
1223
+
1224
+ while (nextRoute != null) {
1225
+ route = nextRoute;
679
1226
  initialRoute = findInitialRoute(route.name, parentScreens, initialRoutes);
680
1227
 
681
1228
  const nestedStateIndex =
682
- nestedState.index || nestedState.routes.length - 1;
1229
+ nestedState.index ?? nestedState.routes.length - 1;
1230
+ const nestedRoute = nestedState.routes[nestedStateIndex];
1231
+
1232
+ if (nestedRoute == null) {
1233
+ throw new Error(`Couldn't find a route at index ${nestedStateIndex}.`);
1234
+ }
683
1235
 
684
- nestedState.routes[nestedStateIndex].state = createStateObject(
1236
+ nestedRoute.state = createStateObject(
685
1237
  initialRoute,
686
1238
  route,
687
1239
  routes.length === 0
688
1240
  );
689
1241
 
690
1242
  if (routes.length > 0) {
691
- nestedState = nestedState.routes[nestedStateIndex]
692
- .state as InitialState;
1243
+ if (nestedRoute.state == null) {
1244
+ throw new Error(
1245
+ `Couldn't find nested state for route '${route.name}'.`
1246
+ );
1247
+ }
1248
+
1249
+ nestedState = nestedRoute.state;
693
1250
  }
694
1251
 
695
1252
  parentScreens.push(route.name);
1253
+ nextRoute = routes.shift();
696
1254
  }
697
1255
  }
698
1256
 
699
- route = findFocusedRoute(state) as ParsedRoute;
700
- route.path = path.replace(/\/$/, '');
701
-
702
- const params = parseQueryParams(
703
- path,
704
- flatConfig ? findParseConfigForRoute(route.name, flatConfig) : undefined
705
- );
706
-
707
- if (params) {
708
- route.params = { ...route.params, ...params };
709
- }
710
-
711
1257
  return state;
712
1258
  };
713
1259
 
714
- const parseQueryParams = (path: string, parseConfig?: ParseConfig) => {
715
- const query = path.split('?')[1];
716
- const params: Record<string, unknown> = queryString.parse(query);
1260
+ const parseQueryParams = (
1261
+ path: string,
1262
+ parseConfig?: ParseConfig,
1263
+ pathParamNames: Set<string> = new Set(),
1264
+ explicitParamNames?: Set<string>,
1265
+ hasNestedScreens = false,
1266
+ routeParams?: object
1267
+ ):
1268
+ | { valid: true; params?: Record<string, unknown> | undefined }
1269
+ | { valid: false } => {
1270
+ const queryIndex = path.indexOf('?');
1271
+ const query = queryIndex === -1 ? undefined : path.slice(queryIndex + 1);
1272
+ const params: Record<string, unknown> = query ? queryString.parse(query) : {};
1273
+
1274
+ // Path params should always win over same-named query params.
1275
+ for (const name of pathParamNames) {
1276
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
1277
+ delete params[name];
1278
+ }
717
1279
 
718
1280
  if (parseConfig) {
719
- Object.keys(params).forEach((name) => {
720
- if (
721
- Object.hasOwnProperty.call(parseConfig, name) &&
722
- parseConfig[name] &&
723
- typeof params[name] === 'string'
724
- ) {
725
- params[name] = parseConfig[name](params[name]);
1281
+ for (const name in parseConfig) {
1282
+ const parser = parseConfig[name];
1283
+
1284
+ if (!parser || pathParamNames.has(name)) {
1285
+ continue;
726
1286
  }
727
- });
1287
+
1288
+ const schema = getStandardSchema(parser);
1289
+
1290
+ if (!Object.hasOwn(params, name)) {
1291
+ if (!schema) {
1292
+ continue;
1293
+ }
1294
+
1295
+ const result = getValidationResult(schema, undefined);
1296
+
1297
+ if (result.issues) {
1298
+ return { valid: false };
1299
+ }
1300
+
1301
+ if (result.value !== undefined) {
1302
+ params[name] = result.value;
1303
+ }
1304
+
1305
+ continue;
1306
+ }
1307
+
1308
+ if (schema) {
1309
+ const result = getValidationResult(schema, params[name]);
1310
+
1311
+ if (result.issues) {
1312
+ return { valid: false };
1313
+ }
1314
+
1315
+ params[name] = result.value;
1316
+ continue;
1317
+ }
1318
+
1319
+ const value = Array.isArray(params[name])
1320
+ ? params[name][0]
1321
+ : params[name];
1322
+
1323
+ if (typeof value !== 'string') {
1324
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
1325
+ delete params[name];
1326
+ continue;
1327
+ }
1328
+
1329
+ if (typeof parser === 'function') {
1330
+ params[name] = parser(value);
1331
+ continue;
1332
+ }
1333
+
1334
+ throw new Error(INVALID_PARSER_ERROR);
1335
+ }
728
1336
  }
729
1337
 
730
- return Object.keys(params).length ? params : undefined;
1338
+ if (
1339
+ hasNestedScreens &&
1340
+ !explicitParamNames?.has('screen') &&
1341
+ (typeof params.screen === 'string' ||
1342
+ (routeParams != null &&
1343
+ 'screen' in routeParams &&
1344
+ typeof routeParams.screen === 'string'))
1345
+ ) {
1346
+ for (const name of NESTED_SCREEN_PARAM_NAMES) {
1347
+ if (!explicitParamNames?.has(name)) {
1348
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
1349
+ delete params[name];
1350
+ }
1351
+ }
1352
+ }
1353
+
1354
+ return {
1355
+ valid: true,
1356
+ params: Object.keys(params).length ? params : undefined,
1357
+ };
731
1358
  };