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

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