@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,33 +1,51 @@
1
- import type {
2
- InitialState,
3
- NavigationState,
4
- PartialState,
5
- } from '@react-navigation/routers';
1
+ import type { NavigationState, PartialState } from '@react-navigation/routers';
6
2
  import escape from 'escape-string-regexp';
7
- import * as queryString from 'query-string';
3
+ import queryString from 'query-string';
8
4
 
9
5
  import { arrayStartsWith } from './arrayStartsWith';
10
- import { findFocusedRoute } from './findFocusedRoute';
11
- import { getPatternParts, type PatternPart } from './getPatternParts';
6
+ import {
7
+ combinePatternParts,
8
+ getPatternParts,
9
+ type PatternPart,
10
+ } from './getPatternParts';
12
11
  import { isArrayEqual } from './isArrayEqual';
13
12
  import type { PathConfig, PathConfigMap } from './types';
13
+ import type {
14
+ StandardSchemaV1,
15
+ StandardSchemaValidationResult,
16
+ } from './utilities';
14
17
  import { validatePathConfig } from './validatePathConfig';
15
18
 
16
19
  type Options<ParamList extends {}> = {
17
- path?: string;
18
- initialRouteName?: string;
20
+ path?: string | undefined;
21
+ initialRouteName?: string | undefined;
19
22
  screens: PathConfigMap<ParamList>;
20
23
  };
21
24
 
22
- type ParseConfig = Record<string, ((value: string) => unknown) | undefined>;
25
+ type ParseConfigValue =
26
+ | ((value: string) => unknown)
27
+ | StandardSchemaV1<unknown, unknown>;
28
+
29
+ type ParseConfig = Record<string, ParseConfigValue | undefined>;
23
30
 
24
31
  type RouteConfig = {
25
32
  screen: string;
26
- regex?: RegExp;
33
+ order: number;
34
+ regex?: RegExp | undefined;
35
+ pattern: string;
27
36
  segments: string[];
28
- params: { screen: string; name?: string; index: number }[];
37
+ params: {
38
+ screen: string;
39
+ name: string;
40
+ index: number;
41
+ regex?: RegExp | undefined;
42
+ }[];
29
43
  routeNames: string[];
30
- parse?: ParseConfig;
44
+ parse?: ParseConfig | undefined;
45
+ explicitParamNames?: Set<string> | undefined;
46
+ pathParamNames: Set<string>;
47
+ hasNestedScreens: boolean;
48
+ shared: boolean;
31
49
  };
32
50
 
33
51
  type InitialRouteConfig = {
@@ -35,19 +53,95 @@ type InitialRouteConfig = {
35
53
  parentScreens: string[];
36
54
  };
37
55
 
38
- type ResultState = PartialState<NavigationState> & {
39
- state?: ResultState;
40
- };
41
-
42
56
  type ParsedRoute = {
43
57
  name: string;
44
- path?: string;
45
58
  params?: Record<string, unknown> | undefined;
59
+ path?: string | undefined;
46
60
  };
47
61
 
62
+ type RoutePatternPart = PatternPart & { screen: string };
63
+
48
64
  type ConfigResources = {
49
65
  initialRoutes: InitialRouteConfig[];
50
66
  configs: RouteConfig[];
67
+ configsByScreen: Record<string, RouteConfig[]>;
68
+ configsByPattern: Record<string, RouteConfig[]>;
69
+ prefixRegex?: RegExp | undefined;
70
+ };
71
+
72
+ const INVALID_SCHEMA_RESULT_ERROR =
73
+ 'Invalid validation result from schema. It should be an object with either "value" or "issues" property and cannot be asynchronous.';
74
+
75
+ const INVALID_PARSER_ERROR =
76
+ 'Invalid parser. Expected a function or a Standard Schema V1 object.';
77
+
78
+ const PARAM_GROUP_PREFIX = 'param_';
79
+
80
+ const NESTED_SCREEN_PARAM_NAMES = [
81
+ 'screen',
82
+ 'params',
83
+ 'initial',
84
+ 'path',
85
+ 'merge',
86
+ 'pop',
87
+ ];
88
+
89
+ const getStaticSegmentPattern = (segment: string) =>
90
+ Array.from(segment, (char) => {
91
+ const encoded = encodeURIComponent(char);
92
+ const percentEncoded =
93
+ encoded === char
94
+ ? `%${char.charCodeAt(0).toString(16).padStart(2, '0').toUpperCase()}`
95
+ : encoded;
96
+
97
+ return `(?:${escape(char)}|${escape(percentEncoded)})`;
98
+ }).join('');
99
+
100
+ const getExplicitParamNames = (parse?: ParseConfig) => {
101
+ const names: string[] = [];
102
+
103
+ if (parse) {
104
+ for (const name in parse) {
105
+ if (parse[name] != null) {
106
+ names.push(name);
107
+ }
108
+ }
109
+ }
110
+
111
+ return names.length ? new Set(names) : undefined;
112
+ };
113
+
114
+ const getStandardSchema = (parser: ParseConfigValue) => {
115
+ if (
116
+ '~standard' in parser &&
117
+ typeof parser['~standard'] === 'object' &&
118
+ parser['~standard'] !== null &&
119
+ 'version' in parser['~standard'] &&
120
+ parser['~standard'].version === 1 &&
121
+ 'validate' in parser['~standard'] &&
122
+ typeof parser['~standard'].validate === 'function'
123
+ ) {
124
+ return parser['~standard'];
125
+ }
126
+
127
+ return undefined;
128
+ };
129
+
130
+ const getValidationResult = (
131
+ schema: StandardSchemaV1<unknown, unknown>['~standard'],
132
+ value: unknown
133
+ ): StandardSchemaValidationResult<unknown> => {
134
+ const result = schema.validate(value);
135
+
136
+ if (
137
+ result != null &&
138
+ typeof result === 'object' &&
139
+ ('value' in result || ('issues' in result && Array.isArray(result.issues)))
140
+ ) {
141
+ return result;
142
+ }
143
+
144
+ throw new Error(INVALID_SCHEMA_RESULT_ERROR);
51
145
  };
52
146
 
53
147
  /**
@@ -73,44 +167,67 @@ type ConfigResources = {
73
167
  */
74
168
  export function getStateFromPath<ParamList extends {}>(
75
169
  path: string,
76
- options?: Options<ParamList>
77
- ): ResultState | undefined {
78
- const { initialRoutes, configs } = getConfigResources(options);
170
+ options?: Options<ParamList>,
171
+ previous?: NavigationState
172
+ ): PartialState<NavigationState> | undefined {
173
+ const {
174
+ initialRoutes,
175
+ configs,
176
+ configsByScreen,
177
+ configsByPattern,
178
+ prefixRegex,
179
+ } = getConfigResources(options);
79
180
 
80
181
  const screens = options?.screens;
81
182
 
183
+ // When a matched config is shared, the same pattern can resolve to multiple branches
184
+ // pick the one that best matches the current navigation state
185
+ const resolveSharedConfig = (config: RouteConfig) => {
186
+ if (config.shared) {
187
+ const candidates = configsByPattern[config.pattern];
188
+
189
+ if (candidates) {
190
+ return selectSharedConfig(config, candidates, previous, initialRoutes);
191
+ }
192
+ }
193
+
194
+ return config;
195
+ };
196
+
82
197
  let remaining = path
83
198
  .replace(/\/+/g, '/') // Replace multiple slash (//) with single ones
84
199
  .replace(/^\//, '') // Remove extra leading slash
85
- .replace(/\?.*$/, ''); // Remove query params which we will handle later
200
+ .replace(/\?.*$/, '') // Remove query params which we will handle later
201
+ .replace(/%[0-9a-f]{2}/gi, (m) => m.toUpperCase()); // Normalize percent-encoding as hex digits are case-insensitive
86
202
 
87
203
  // Make sure there is a trailing slash
88
204
  remaining = remaining.endsWith('/') ? remaining : `${remaining}/`;
89
205
 
90
- const prefix = options?.path?.replace(/^\//, ''); // Remove extra leading slash
206
+ if (prefixRegex) {
207
+ const prefixMatch = remaining.match(prefixRegex);
91
208
 
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)) {
209
+ if (prefixMatch == null) {
98
210
  return undefined;
99
211
  }
100
212
 
101
- // Remove the prefix from the path
102
- remaining = remaining.replace(normalizedPrefix, '');
213
+ remaining = remaining.slice(prefixMatch[0].length);
103
214
  }
104
215
 
105
216
  if (screens === undefined) {
106
217
  // 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
- });
218
+ const routes: ParsedRoute[] = [];
219
+
220
+ for (const segment of remaining.split('/')) {
221
+ if (!segment) {
222
+ continue;
223
+ }
224
+
225
+ try {
226
+ routes.push({ name: decodeURIComponent(segment) });
227
+ } catch {
228
+ return undefined;
229
+ }
230
+ }
114
231
 
115
232
  if (routes.length) {
116
233
  return createNestedStateObject(path, routes, initialRoutes);
@@ -122,41 +239,105 @@ export function getStateFromPath<ParamList extends {}>(
122
239
  if (remaining === '/') {
123
240
  // We need to add special handling of empty path so navigation to empty path also works
124
241
  // When handling empty path, we should only look at the root level config
125
- const match = configs.find((config) => config.segments.join('/') === '');
242
+ const match = configs.find((config) => config.pattern === '');
126
243
 
127
244
  if (match) {
245
+ const config = resolveSharedConfig(match);
246
+
128
247
  return createNestedStateObject(
129
248
  path,
130
- match.routeNames.map((name) => ({ name })),
249
+ config.routeNames.map((name) => ({ name })),
131
250
  initialRoutes,
132
- configs
251
+ config
133
252
  );
134
253
  }
135
254
 
136
255
  return undefined;
137
256
  }
138
257
 
139
- let result: PartialState<NavigationState> | undefined;
140
- let current: PartialState<NavigationState> | undefined;
141
-
142
258
  // We match the whole path against the regex instead of segments
143
259
  // This makes sure matches such as wildcard will catch any unmatched routes, even if nested
144
- const { routes, remainingPath } = matchAgainstConfigs(remaining, configs);
260
+ const firstRawSegment = remaining.split('/')[0];
261
+
262
+ let firstDecodedSegment: string | undefined;
145
263
 
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;
264
+ try {
265
+ firstDecodedSegment =
266
+ firstRawSegment != null ? decodeURIComponent(firstRawSegment) : undefined;
267
+ } catch {
268
+ firstDecodedSegment = undefined;
151
269
  }
152
270
 
153
- if (current == null || result == null) {
154
- return undefined;
271
+ for (const config of configs) {
272
+ // Regex matching is expensive, so skip configs that can't match this path.
273
+ if (
274
+ !canMatchFirstSegment(
275
+ config.segments[0],
276
+ firstDecodedSegment,
277
+ firstRawSegment
278
+ )
279
+ ) {
280
+ continue;
281
+ }
282
+
283
+ const routes = matchAgainstConfig(remaining, config, configsByScreen);
284
+
285
+ if (routes === undefined) {
286
+ continue;
287
+ }
288
+
289
+ let selectedConfig = resolveSharedConfig(config);
290
+ let selectedRoutes = routes;
291
+
292
+ if (selectedConfig !== config) {
293
+ const rematchedRoutes = matchAgainstConfig(
294
+ remaining,
295
+ selectedConfig,
296
+ configsByScreen
297
+ );
298
+
299
+ if (rematchedRoutes === undefined) {
300
+ selectedConfig = config;
301
+ } else {
302
+ selectedRoutes = rematchedRoutes;
303
+ }
304
+ }
305
+
306
+ const state = createNestedStateObject(
307
+ path,
308
+ selectedRoutes,
309
+ initialRoutes,
310
+ selectedConfig
311
+ );
312
+
313
+ if (state !== undefined) {
314
+ return state;
315
+ }
155
316
  }
156
317
 
157
- return result;
318
+ return undefined;
158
319
  }
159
320
 
321
+ const canMatchFirstSegment = (
322
+ configSegment: string | undefined,
323
+ decodedSegment: string | undefined,
324
+ rawSegment: string | undefined
325
+ ) => {
326
+ if (decodedSegment === undefined) {
327
+ return true;
328
+ }
329
+
330
+ if (configSegment === undefined) {
331
+ return false;
332
+ }
333
+
334
+ if (configSegment === '*' || configSegment.startsWith(':')) {
335
+ return true;
336
+ }
337
+
338
+ return configSegment === decodedSegment || configSegment === rawSegment;
339
+ };
340
+
160
341
  /**
161
342
  * Reference to the last used config resources. This is used to avoid recomputing the config resources when the options are the same.
162
343
  */
@@ -165,11 +346,17 @@ const cachedConfigResources = new WeakMap<Options<{}>, ConfigResources>();
165
346
  function getConfigResources<ParamList extends {}>(
166
347
  options: Options<ParamList> | undefined
167
348
  ) {
168
- if (!options) return prepareConfigResources();
349
+ if (!options) {
350
+ return prepareConfigResources();
351
+ }
169
352
 
170
353
  const cached = cachedConfigResources.get(options);
171
354
 
172
- if (cached) return cached;
355
+ if (cached) {
356
+ return cached;
357
+ }
358
+
359
+ validatePathConfig(options);
173
360
 
174
361
  const resources = prepareConfigResources(options);
175
362
 
@@ -179,21 +366,44 @@ function getConfigResources<ParamList extends {}>(
179
366
  }
180
367
 
181
368
  function prepareConfigResources(options?: Options<{}>) {
182
- if (options) {
183
- validatePathConfig(options);
184
- }
185
-
186
369
  const initialRoutes = getInitialRoutes(options);
187
370
  const configs = getSortedNormalizedConfigs(initialRoutes, options?.screens);
371
+ const prefix = options?.path?.replace(/^\//, '');
372
+
373
+ let prefixRegex: RegExp | undefined;
374
+
375
+ if (prefix) {
376
+ const normalizedPrefix = prefix.endsWith('/') ? prefix : `${prefix}/`;
377
+ const prefixPattern = normalizedPrefix
378
+ .split('/')
379
+ .map(getStaticSegmentPattern)
380
+ .join('/');
381
+
382
+ prefixRegex = new RegExp(`^${prefixPattern}`);
383
+ }
188
384
 
189
- checkForDuplicatedConfigs(configs);
385
+ const configsByScreen: Record<string, RouteConfig[]> = {};
386
+ const configsByPattern: Record<string, RouteConfig[]> = {};
190
387
 
191
- const configWithRegexes = getConfigsWithRegexes(configs);
388
+ for (const c of configs) {
389
+ (configsByScreen[c.screen] ??= []).push(c);
390
+
391
+ const existing = configsByPattern[c.pattern];
392
+
393
+ if (existing) {
394
+ checkForDuplicatedConfigs(existing[existing.length - 1], c);
395
+ existing.push(c);
396
+ } else {
397
+ configsByPattern[c.pattern] = [c];
398
+ }
399
+ }
192
400
 
193
401
  return {
194
402
  initialRoutes,
195
403
  configs,
196
- configWithRegexes,
404
+ configsByScreen,
405
+ configsByPattern,
406
+ prefixRegex,
197
407
  };
198
408
  }
199
409
 
@@ -215,211 +425,267 @@ function getSortedNormalizedConfigs(
215
425
  screens: Record<string, string | PathConfig<{}>> = {}
216
426
  ) {
217
427
  // 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
- }
237
-
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)) {
428
+ const configs: RouteConfig[] = [];
429
+
430
+ for (const key in screens) {
431
+ configs.push(
432
+ ...createNormalizedConfigs(key, screens, initialRoutes, [], [], [])
433
+ );
434
+ }
435
+
436
+ for (let order = 0; order < configs.length; order++) {
437
+ const config = configs[order];
438
+
439
+ if (config) {
440
+ config.order = order;
441
+ }
442
+ }
443
+
444
+ return configs.sort((a, b) => {
445
+ // Sort config from most specific to least specific:
446
+ // - more segments
447
+ // - static segments
448
+ // - params with regex
449
+ // - regular params
450
+ // - wildcard
451
+
452
+ // If 2 patterns are same, move the one with less route names up
453
+ // This is an error state, so it's only useful for consistent error messages
454
+ if (isArrayEqual(a.segments, b.segments)) {
455
+ if (
456
+ a.routeNames.length > b.routeNames.length &&
457
+ arrayStartsWith(a.routeNames, b.routeNames)
458
+ ) {
241
459
  return -1;
242
460
  }
243
461
 
244
- if (arrayStartsWith(b.segments, a.segments)) {
462
+ if (
463
+ b.routeNames.length > a.routeNames.length &&
464
+ arrayStartsWith(b.routeNames, a.routeNames)
465
+ ) {
245
466
  return 1;
246
467
  }
247
468
 
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
- }
469
+ return a.routeNames.length - b.routeNames.length || a.order - b.order;
470
+ }
253
471
 
254
- // if a is longer, a gets higher priority
255
- if (b.segments[i] == null) {
256
- return -1;
257
- }
472
+ // If one of the patterns starts with the other, it's more exhaustive
473
+ // So move it up
474
+ if (arrayStartsWith(a.segments, b.segments)) {
475
+ return -1;
476
+ }
258
477
 
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('(');
478
+ if (arrayStartsWith(b.segments, a.segments)) {
479
+ return 1;
480
+ }
265
481
 
266
- // if both are wildcard or regex, we compare next component
267
- if ((aWildCard && bWildCard) || (aRegex && bRegex)) {
268
- continue;
269
- }
482
+ for (let i = 0; i < Math.max(a.segments.length, b.segments.length); i++) {
483
+ const aSegment = a.segments[i];
484
+ const bSegment = b.segments[i];
270
485
 
271
- // if only a is wildcard, b gets higher priority
272
- if (aWildCard && !bWildCard) {
273
- return 1;
274
- }
486
+ // if b is longer, b gets higher priority
487
+ if (aSegment == null) {
488
+ return 1;
489
+ }
275
490
 
276
- // if only b is wildcard, a gets higher priority
277
- if (bWildCard && !aWildCard) {
278
- return -1;
279
- }
491
+ // if a is longer, a gets higher priority
492
+ if (bSegment == null) {
493
+ return -1;
494
+ }
280
495
 
281
- // If only a has a param, b gets higher priority
282
- if (aParam && !bParam) {
283
- return 1;
284
- }
496
+ const aWildCard = aSegment === '*';
497
+ const bWildCard = bSegment === '*';
498
+ const aParam = aSegment.startsWith(':');
499
+ const bParam = bSegment.startsWith(':');
500
+ const aRegex = aParam && aSegment.includes('(');
501
+ const bRegex = bParam && bSegment.includes('(');
285
502
 
286
- // If only b has a param, a gets higher priority
287
- if (bParam && !aParam) {
288
- return -1;
289
- }
503
+ // if both are wildcard or regex, we compare next component
504
+ if ((aWildCard && bWildCard) || (aRegex && bRegex)) {
505
+ continue;
506
+ }
290
507
 
291
- // if only a has regex, a gets higher priority
292
- if (aRegex && !bRegex) {
293
- return -1;
294
- }
508
+ // if only a is wildcard, b gets higher priority
509
+ if (aWildCard && !bWildCard) {
510
+ return 1;
511
+ }
295
512
 
296
- // if only b has regex, b gets higher priority
297
- if (bRegex && !aRegex) {
298
- return 1;
299
- }
513
+ // if only b is wildcard, a gets higher priority
514
+ if (bWildCard && !aWildCard) {
515
+ return -1;
300
516
  }
301
517
 
302
- return a.segments.length - b.segments.length;
303
- });
304
- }
518
+ // If only a has a param, b gets higher priority
519
+ if (aParam && !bParam) {
520
+ return 1;
521
+ }
305
522
 
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
- );
523
+ // If only b has a param, a gets higher priority
524
+ if (bParam && !aParam) {
525
+ return -1;
526
+ }
527
+
528
+ // if only a has regex, a gets higher priority
529
+ if (aRegex && !bRegex) {
530
+ return -1;
531
+ }
532
+
533
+ // if only b has regex, b gets higher priority
534
+ if (bRegex && !aRegex) {
535
+ return 1;
330
536
  }
331
537
  }
332
538
 
333
- return Object.assign(acc, {
334
- [pattern]: config,
335
- });
336
- }, {});
539
+ return a.segments.length - b.segments.length;
540
+ });
337
541
  }
338
542
 
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
- }));
543
+ // Throw if two configs resolve to the same pattern but conflicting screens
544
+ function checkForDuplicatedConfigs(
545
+ existing: RouteConfig | undefined,
546
+ config: RouteConfig
547
+ ) {
548
+ if (!existing) {
549
+ return;
550
+ }
551
+
552
+ const a = existing.routeNames;
553
+ const b = config.routeNames;
554
+
555
+ // It's not a problem if the path string omitted from a inner most screen
556
+ // For example, it's ok if a path resolves to `A > B > C` or `A > B`
557
+ const intersects =
558
+ a.length > b.length ? arrayStartsWith(a, b) : arrayStartsWith(b, a);
559
+
560
+ if (!intersects && (!existing.shared || !config.shared)) {
561
+ throw new Error(
562
+ `Found conflicting screens with the same pattern. The pattern '${
563
+ config.pattern
564
+ }' resolves to both '${a.join(' > ')}' and '${b.join(
565
+ ' > '
566
+ )}'. Patterns must be unique and cannot resolve to more than one screen unless shared: true is specified.`
567
+ );
568
+ }
345
569
  }
346
570
 
347
- const matchAgainstConfigs = (remaining: string, configs: RouteConfig[]) => {
348
- let routes: ParsedRoute[] | undefined;
349
- let remainingPath = remaining;
571
+ const matchAgainstConfig = (
572
+ remaining: string,
573
+ config: RouteConfig,
574
+ configsByScreen: Record<string, RouteConfig[]>
575
+ ) => {
576
+ if (!config.regex) {
577
+ return undefined;
578
+ }
350
579
 
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
- }
580
+ const match = remaining.match(config.regex);
356
581
 
357
- const match = remainingPath.match(config.regex);
582
+ if (!match) {
583
+ return undefined;
584
+ }
358
585
 
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
- });
586
+ let validationFailed = false;
587
+ const matchedRoutes: ParsedRoute[] = [];
369
588
 
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;
589
+ for (const [index, routeName] of config.routeNames.entries()) {
590
+ // Check matching name, route names and pattern in case same screen is used
591
+ // at different levels or branches in config
592
+ const routeConfig = configsByScreen[routeName]?.find(
593
+ (c) =>
594
+ index === c.routeNames.length - 1 &&
595
+ arrayStartsWith(config.routeNames, c.routeNames) &&
596
+ arrayStartsWith(config.segments, c.segments)
597
+ );
598
+
599
+ let params: Record<string, unknown> | undefined;
600
+ let hasParams = false;
401
601
 
402
- if (params && Object.keys(params).length) {
403
- return { name: routeName, params };
602
+ if (routeConfig && match.groups) {
603
+ params = {};
604
+
605
+ for (const param of routeConfig.params) {
606
+ if (param.screen !== routeName) {
607
+ continue;
404
608
  }
405
609
 
406
- return { name: routeName };
407
- });
610
+ const value = match.groups[`${PARAM_GROUP_PREFIX}${param.index}`];
408
611
 
409
- remainingPath = remainingPath.replace(match[0], '');
612
+ if (value == null) {
613
+ params[param.name] = undefined;
614
+ hasParams = true;
615
+ continue;
616
+ }
410
617
 
411
- break;
618
+ let decoded: string;
619
+
620
+ try {
621
+ decoded = decodeURIComponent(value);
622
+ } catch {
623
+ return undefined;
624
+ }
625
+
626
+ // Percent-encoded values are matched permissively
627
+ // So validate the decoded value against the custom regex
628
+ if (param.regex && value !== decoded && !param.regex.test(decoded)) {
629
+ return undefined;
630
+ }
631
+
632
+ const parser = routeConfig.parse?.[param.name];
633
+
634
+ if (!parser) {
635
+ params[param.name] = decoded;
636
+ hasParams = true;
637
+ continue;
638
+ }
639
+
640
+ const schema = getStandardSchema(parser);
641
+
642
+ if (schema) {
643
+ const result = getValidationResult(schema, decoded);
644
+
645
+ if (result.issues) {
646
+ validationFailed = true;
647
+ break;
648
+ }
649
+
650
+ params[param.name] = result.value;
651
+ hasParams = true;
652
+ continue;
653
+ }
654
+
655
+ if (typeof parser === 'function') {
656
+ params[param.name] = parser(decoded);
657
+ hasParams = true;
658
+ continue;
659
+ }
660
+
661
+ throw new Error(INVALID_PARSER_ERROR);
662
+ }
663
+
664
+ if (validationFailed) {
665
+ // A failed param validation invalidates the whole nested route chain for this config.
666
+ break;
667
+ }
668
+ }
669
+
670
+ if (params && hasParams) {
671
+ matchedRoutes.push({ name: routeName, params });
672
+ } else {
673
+ matchedRoutes.push({ name: routeName });
412
674
  }
413
675
  }
414
676
 
415
- return { routes, remainingPath };
677
+ if (validationFailed) {
678
+ return undefined;
679
+ }
680
+
681
+ return matchedRoutes;
416
682
  };
417
683
 
418
684
  const createNormalizedConfigs = (
419
685
  screen: string,
420
686
  routeConfig: Record<string, string | PathConfig<{}>>,
421
687
  initials: InitialRouteConfig[],
422
- paths: { screen: string; path: string }[],
688
+ parts: RoutePatternPart[],
423
689
  parentScreens: string[],
424
690
  routeNames: string[]
425
691
  ): RouteConfig[] => {
@@ -432,19 +698,31 @@ const createNormalizedConfigs = (
432
698
  const config = routeConfig[screen];
433
699
 
434
700
  if (typeof config === 'string') {
435
- paths.push({ screen, path: config });
436
- configs.push(createConfigItem(screen, [...routeNames], [...paths]));
701
+ const configParts = combinePatternParts(
702
+ parts,
703
+ getPatternParts(config).map((part) => ({ ...part, screen }))
704
+ );
705
+
706
+ configs.push(createConfigItem(screen, [...routeNames], configParts));
437
707
  } else if (typeof config === 'object') {
438
708
  // if an object is specified as the value (e.g. Foo: { ... }),
439
709
  // it can have `path` property and
440
710
  // 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
- }
711
+ const nestedScreens =
712
+ 'screens' in config
713
+ ? (config.screens as Record<string, string | PathConfig<{}>>)
714
+ : undefined;
715
+ const hasNestedScreens = !!nestedScreens;
716
+ const currentParts =
717
+ typeof config.path === 'string'
718
+ ? combinePatternParts(
719
+ parts,
720
+ getPatternParts(config.path).map((part) => ({ ...part, screen })),
721
+ config.exact
722
+ )
723
+ : parts;
447
724
 
725
+ if (typeof config.path === 'string') {
448
726
  // We should add alias configs after the main config
449
727
  // So unless they are more specific, main config will be matched first
450
728
  const aliasConfigs = [];
@@ -456,8 +734,13 @@ const createNormalizedConfigs = (
456
734
  createConfigItem(
457
735
  screen,
458
736
  [...routeNames],
459
- [...paths, { screen, path: alias }],
460
- config.parse
737
+ combinePatternParts(
738
+ parts,
739
+ getPatternParts(alias).map((part) => ({ ...part, screen }))
740
+ ),
741
+ config.parse,
742
+ hasNestedScreens,
743
+ false
461
744
  )
462
745
  );
463
746
  } else if (typeof alias === 'object') {
@@ -465,25 +748,32 @@ const createNormalizedConfigs = (
465
748
  createConfigItem(
466
749
  screen,
467
750
  [...routeNames],
468
- alias.exact
469
- ? [{ screen, path: alias.path }]
470
- : [...paths, { screen, path: alias.path }],
471
- alias.parse
751
+ combinePatternParts(
752
+ parts,
753
+ getPatternParts(alias.path).map((part) => ({
754
+ ...part,
755
+ screen,
756
+ })),
757
+ alias.exact
758
+ ),
759
+ alias.parse,
760
+ hasNestedScreens,
761
+ alias.shared === true
472
762
  )
473
763
  );
474
764
  }
475
765
  }
476
766
  }
477
767
 
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
768
  configs.push(
486
- createConfigItem(screen, [...routeNames], [...paths], config.parse)
769
+ createConfigItem(
770
+ screen,
771
+ [...routeNames],
772
+ currentParts,
773
+ config.parse,
774
+ hasNestedScreens,
775
+ config.shared === true
776
+ )
487
777
  );
488
778
 
489
779
  configs.push(...aliasConfigs);
@@ -499,7 +789,7 @@ const createNormalizedConfigs = (
499
789
  );
500
790
  }
501
791
 
502
- if ('screens' in config && config.screens) {
792
+ if (nestedScreens) {
503
793
  // property `initialRouteName` without `screens` has no purpose
504
794
  if (
505
795
  'initialRouteName' in config &&
@@ -511,18 +801,18 @@ const createNormalizedConfigs = (
511
801
  });
512
802
  }
513
803
 
514
- Object.keys(config.screens).forEach((nestedConfig) => {
804
+ for (const nestedConfig in nestedScreens) {
515
805
  const result = createNormalizedConfigs(
516
806
  nestedConfig,
517
- config.screens as Record<string, string | PathConfig<{}>>,
807
+ nestedScreens,
518
808
  initials,
519
- [...paths],
809
+ currentParts,
520
810
  [...parentScreens],
521
811
  routeNames
522
812
  );
523
813
 
524
814
  configs.push(...result);
525
- });
815
+ }
526
816
  }
527
817
  }
528
818
 
@@ -534,66 +824,150 @@ const createNormalizedConfigs = (
534
824
  const createConfigItem = (
535
825
  screen: string,
536
826
  routeNames: string[],
537
- paths: { screen: string; path: string }[],
538
- parse?: ParseConfig
827
+ parts: RoutePatternPart[],
828
+ parse?: ParseConfig,
829
+ hasNestedScreens = false,
830
+ shared = false
539
831
  ): RouteConfig => {
540
- const parts: (PatternPart & { screen: string })[] = [];
832
+ let regex: RegExp | undefined;
833
+
834
+ const segments: string[] = [];
835
+ const params: RouteConfig['params'] = [];
836
+ const pathParamNames = new Set<string>();
837
+
838
+ if (parts.length) {
839
+ let regexString = '^(';
840
+ let index = 0;
841
+
842
+ for (const part of parts) {
843
+ segments.push(part.segment);
844
+
845
+ if (part.param) {
846
+ // A custom regex may not match the percent-encoded form of the value
847
+ // So also accept segments containing an encoded character and validate them after decoding
848
+ const reg = part.regex
849
+ ? `(?:${part.regex})|(?=[^/]*%[0-9A-F]{2})[^/]+`
850
+ : '[^/]+';
851
+
852
+ regexString += `(((?<${PARAM_GROUP_PREFIX}${index}>${reg})\\/)${part.optional ? '?' : ''})`;
853
+ params.push({
854
+ index,
855
+ screen: part.screen,
856
+ name: part.param,
857
+ regex: part.regex ? new RegExp(`^(?:${part.regex})$`) : undefined,
858
+ });
541
859
 
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
- }
860
+ if (part.screen === screen) {
861
+ pathParamNames.add(part.param);
862
+ }
863
+ } else if (part.segment === '*') {
864
+ regexString += `.*\\/`;
865
+ } else {
866
+ regexString += `${getStaticSegmentPattern(part.segment)}\\/`;
867
+ }
546
868
 
547
- const regex = parts.length
548
- ? new RegExp(
549
- `^(${parts
550
- .map((it, i) => {
551
- if (it.param) {
552
- const reg = it.regex || '[^/]+';
869
+ index++;
870
+ }
553
871
 
554
- return `(((?<param_${i}>${reg})\\/)${it.optional ? '?' : ''})`;
555
- }
872
+ regexString += ')$';
873
+ regex = new RegExp(regexString);
874
+ }
556
875
 
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);
876
+ const pattern = segments.join('/');
575
877
 
576
878
  return {
577
879
  screen,
880
+ order: 0,
578
881
  regex,
882
+ pattern,
579
883
  segments,
580
884
  params,
581
885
  routeNames,
582
886
  parse,
887
+ explicitParamNames: getExplicitParamNames(parse),
888
+ pathParamNames,
889
+ hasNestedScreens,
890
+ shared,
583
891
  };
584
892
  };
585
893
 
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;
894
+ const selectSharedConfig = (
895
+ match: RouteConfig,
896
+ candidates: RouteConfig[],
897
+ previous: NavigationState | PartialState<NavigationState> | undefined,
898
+ initialRoutes: InitialRouteConfig[]
899
+ ) => {
900
+ // Walk the previous state down to the focused leaf to get the active route path
901
+ const focusedRouteNames: string[] = [];
902
+
903
+ let current = previous;
904
+
905
+ while (current?.routes.length) {
906
+ const index =
907
+ typeof current.index === 'number'
908
+ ? current.index
909
+ : current.routes.length - 1;
910
+
911
+ const route = current.routes[index];
912
+
913
+ if (route == null) {
914
+ break;
593
915
  }
916
+
917
+ focusedRouteNames.push(route.name);
918
+ current = route.state;
594
919
  }
595
920
 
596
- return undefined;
921
+ // Rank a candidate by how far it aligns with the focused path, then by
922
+ // how many of its levels match a configured initial route
923
+ const score = (routeNames: string[]) => {
924
+ let focused = 0;
925
+
926
+ while (
927
+ routeNames[focused] !== undefined &&
928
+ routeNames[focused] === focusedRouteNames[focused]
929
+ ) {
930
+ focused++;
931
+ }
932
+
933
+ let initial = 0;
934
+
935
+ for (const config of initialRoutes) {
936
+ if (
937
+ arrayStartsWith(routeNames, config.parentScreens) &&
938
+ routeNames[config.parentScreens.length] === config.initialRouteName
939
+ ) {
940
+ initial++;
941
+ }
942
+ }
943
+
944
+ return { focused, initial };
945
+ };
946
+
947
+ let selected = match;
948
+ let best = { focused: -1, initial: -1 };
949
+
950
+ for (const candidate of candidates) {
951
+ if (!candidate.shared) {
952
+ continue;
953
+ }
954
+
955
+ const { focused, initial } = score(candidate.routeNames);
956
+
957
+ const better =
958
+ focused !== best.focused
959
+ ? focused > best.focused
960
+ : initial !== best.initial
961
+ ? initial > best.initial
962
+ : candidate.order < selected.order;
963
+
964
+ if (better) {
965
+ selected = candidate;
966
+ best = { focused, initial };
967
+ }
968
+ }
969
+
970
+ return selected;
597
971
  };
598
972
 
599
973
  // Try to find an initial route connected with the one passed
@@ -606,7 +980,14 @@ const findInitialRoute = (
606
980
  if (parentScreens.length === config.parentScreens.length) {
607
981
  let sameParents = true;
608
982
  for (let i = 0; i < parentScreens.length; i++) {
609
- if (parentScreens[i].localeCompare(config.parentScreens[i]) !== 0) {
983
+ const parentScreen = parentScreens[i];
984
+ const configParentScreen = config.parentScreens[i];
985
+
986
+ if (
987
+ parentScreen == null ||
988
+ configParentScreen == null ||
989
+ parentScreen.localeCompare(configParentScreen) !== 0
990
+ ) {
610
991
  sameParents = false;
611
992
  break;
612
993
  }
@@ -627,7 +1008,7 @@ const createStateObject = (
627
1008
  initialRoute: string | undefined,
628
1009
  route: ParsedRoute,
629
1010
  isEmpty: boolean
630
- ): InitialState => {
1011
+ ): PartialState<NavigationState> => {
631
1012
  if (isEmpty) {
632
1013
  if (initialRoute) {
633
1014
  return {
@@ -657,75 +1038,184 @@ const createNestedStateObject = (
657
1038
  path: string,
658
1039
  routes: ParsedRoute[],
659
1040
  initialRoutes: InitialRouteConfig[],
660
- flatConfig?: RouteConfig[]
661
- ) => {
662
- let route = routes.shift() as ParsedRoute;
1041
+ routeConfig?: RouteConfig
1042
+ ): PartialState<NavigationState> | undefined => {
1043
+ const focusedRoute = routes[routes.length - 1];
1044
+
1045
+ if (focusedRoute == null) {
1046
+ return undefined;
1047
+ }
1048
+
1049
+ focusedRoute.path = path.replace(/\/$/, '');
1050
+
1051
+ const queryParams = parseQueryParams(
1052
+ path,
1053
+ routeConfig?.parse,
1054
+ routeConfig?.pathParamNames,
1055
+ routeConfig?.explicitParamNames,
1056
+ routeConfig?.hasNestedScreens,
1057
+ focusedRoute.params
1058
+ );
1059
+
1060
+ if (!queryParams.valid) {
1061
+ return undefined;
1062
+ }
1063
+
1064
+ if (queryParams.params) {
1065
+ focusedRoute.params = { ...focusedRoute.params, ...queryParams.params };
1066
+ }
1067
+
1068
+ let route = routes.shift();
1069
+
1070
+ if (route == null) {
1071
+ return undefined;
1072
+ }
1073
+
663
1074
  const parentScreens: string[] = [];
664
1075
 
665
1076
  let initialRoute = findInitialRoute(route.name, parentScreens, initialRoutes);
666
1077
 
667
1078
  parentScreens.push(route.name);
668
1079
 
669
- const state: InitialState = createStateObject(
670
- initialRoute,
671
- route,
672
- routes.length === 0
673
- );
1080
+ const state = createStateObject(initialRoute, route, routes.length === 0);
674
1081
 
675
1082
  if (routes.length > 0) {
676
1083
  let nestedState = state;
677
1084
 
678
- while ((route = routes.shift() as ParsedRoute)) {
1085
+ let nextRoute = routes.shift();
1086
+
1087
+ while (nextRoute != null) {
1088
+ route = nextRoute;
679
1089
  initialRoute = findInitialRoute(route.name, parentScreens, initialRoutes);
680
1090
 
681
1091
  const nestedStateIndex =
682
1092
  nestedState.index || nestedState.routes.length - 1;
1093
+ const nestedRoute = nestedState.routes[nestedStateIndex];
683
1094
 
684
- nestedState.routes[nestedStateIndex].state = createStateObject(
1095
+ if (nestedRoute == null) {
1096
+ throw new Error(`Couldn't find a route at index ${nestedStateIndex}.`);
1097
+ }
1098
+
1099
+ nestedRoute.state = createStateObject(
685
1100
  initialRoute,
686
1101
  route,
687
1102
  routes.length === 0
688
1103
  );
689
1104
 
690
1105
  if (routes.length > 0) {
691
- nestedState = nestedState.routes[nestedStateIndex]
692
- .state as InitialState;
1106
+ if (nestedRoute.state == null) {
1107
+ throw new Error(
1108
+ `Couldn't find nested state for route '${route.name}'.`
1109
+ );
1110
+ }
1111
+
1112
+ nestedState = nestedRoute.state;
693
1113
  }
694
1114
 
695
1115
  parentScreens.push(route.name);
1116
+ nextRoute = routes.shift();
696
1117
  }
697
1118
  }
698
1119
 
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
1120
  return state;
712
1121
  };
713
1122
 
714
- const parseQueryParams = (path: string, parseConfig?: ParseConfig) => {
715
- const query = path.split('?')[1];
716
- const params: Record<string, unknown> = queryString.parse(query);
1123
+ const parseQueryParams = (
1124
+ path: string,
1125
+ parseConfig?: ParseConfig,
1126
+ pathParamNames: Set<string> = new Set(),
1127
+ explicitParamNames?: Set<string>,
1128
+ hasNestedScreens = false,
1129
+ routeParams?: object
1130
+ ):
1131
+ | { valid: true; params?: Record<string, unknown> | undefined }
1132
+ | { valid: false } => {
1133
+ const queryIndex = path.indexOf('?');
1134
+ const query = queryIndex === -1 ? undefined : path.slice(queryIndex + 1);
1135
+ const params: Record<string, unknown> = query ? queryString.parse(query) : {};
1136
+
1137
+ // Path params should always win over same-named query params.
1138
+ for (const name of pathParamNames) {
1139
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
1140
+ delete params[name];
1141
+ }
717
1142
 
718
1143
  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]);
1144
+ for (const name in parseConfig) {
1145
+ const parser = parseConfig[name];
1146
+
1147
+ if (!parser || pathParamNames.has(name)) {
1148
+ continue;
726
1149
  }
727
- });
1150
+
1151
+ const schema = getStandardSchema(parser);
1152
+
1153
+ if (!Object.hasOwn(params, name)) {
1154
+ if (!schema) {
1155
+ continue;
1156
+ }
1157
+
1158
+ const result = getValidationResult(schema, undefined);
1159
+
1160
+ if (result.issues) {
1161
+ return { valid: false };
1162
+ }
1163
+
1164
+ if (result.value !== undefined) {
1165
+ params[name] = result.value;
1166
+ }
1167
+
1168
+ continue;
1169
+ }
1170
+
1171
+ if (schema) {
1172
+ const result = getValidationResult(schema, params[name]);
1173
+
1174
+ if (result.issues) {
1175
+ return { valid: false };
1176
+ }
1177
+
1178
+ params[name] = result.value;
1179
+ continue;
1180
+ }
1181
+
1182
+ const value = Array.isArray(params[name])
1183
+ ? params[name][0]
1184
+ : params[name];
1185
+
1186
+ if (typeof value !== 'string') {
1187
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
1188
+ delete params[name];
1189
+ continue;
1190
+ }
1191
+
1192
+ if (typeof parser === 'function') {
1193
+ params[name] = parser(value);
1194
+ continue;
1195
+ }
1196
+
1197
+ throw new Error(INVALID_PARSER_ERROR);
1198
+ }
728
1199
  }
729
1200
 
730
- return Object.keys(params).length ? params : undefined;
1201
+ if (
1202
+ hasNestedScreens &&
1203
+ !explicitParamNames?.has('screen') &&
1204
+ (typeof params.screen === 'string' ||
1205
+ (routeParams != null &&
1206
+ 'screen' in routeParams &&
1207
+ typeof routeParams.screen === 'string'))
1208
+ ) {
1209
+ for (const name of NESTED_SCREEN_PARAM_NAMES) {
1210
+ if (!explicitParamNames?.has(name)) {
1211
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
1212
+ delete params[name];
1213
+ }
1214
+ }
1215
+ }
1216
+
1217
+ return {
1218
+ valid: true,
1219
+ params: Object.keys(params).length ? params : undefined,
1220
+ };
731
1221
  };