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

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