one 1.1.473 → 1.1.475

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 (245) hide show
  1. package/dist/cjs/Root.cjs +5 -1
  2. package/dist/cjs/Root.js +2 -2
  3. package/dist/cjs/Root.js.map +1 -1
  4. package/dist/cjs/Root.native.js +5 -2
  5. package/dist/cjs/Root.native.js.map +2 -2
  6. package/dist/cjs/createApp.cjs +2 -0
  7. package/dist/cjs/createApp.js +16 -2
  8. package/dist/cjs/createApp.js.map +1 -1
  9. package/dist/cjs/createApp.native.js +1 -0
  10. package/dist/cjs/createApp.native.js.map +2 -2
  11. package/dist/cjs/fork/__tests__/getPathFromState.test.cjs +1440 -0
  12. package/dist/cjs/fork/__tests__/getPathFromState.test.js +1559 -0
  13. package/dist/cjs/fork/__tests__/getPathFromState.test.js.map +6 -0
  14. package/dist/cjs/fork/__tests__/getPathFromState.test.native.js +1726 -0
  15. package/dist/cjs/fork/__tests__/getPathFromState.test.native.js.map +6 -0
  16. package/dist/cjs/fork/__tests__/getStateFromPath.test.cjs +2565 -0
  17. package/dist/cjs/fork/__tests__/getStateFromPath.test.js +2702 -0
  18. package/dist/cjs/fork/__tests__/getStateFromPath.test.js.map +6 -0
  19. package/dist/cjs/fork/__tests__/getStateFromPath.test.native.js +2861 -0
  20. package/dist/cjs/fork/__tests__/getStateFromPath.test.native.js.map +6 -0
  21. package/dist/cjs/fork/getPathFromState.cjs +2 -1
  22. package/dist/cjs/fork/getPathFromState.js +1 -1
  23. package/dist/cjs/fork/getPathFromState.js.map +1 -1
  24. package/dist/cjs/fork/getPathFromState.native.js +10 -5
  25. package/dist/cjs/fork/getPathFromState.native.js.map +1 -1
  26. package/dist/cjs/fork/getPathFromState.test.cjs +113 -0
  27. package/dist/cjs/fork/getPathFromState.test.js +122 -0
  28. package/dist/cjs/fork/getPathFromState.test.js.map +6 -0
  29. package/dist/cjs/fork/getPathFromState.test.native.js +135 -0
  30. package/dist/cjs/fork/getPathFromState.test.native.js.map +6 -0
  31. package/dist/cjs/fork/getStateFromPath.test.cjs +229 -0
  32. package/dist/cjs/fork/getStateFromPath.test.js +290 -0
  33. package/dist/cjs/fork/getStateFromPath.test.js.map +6 -0
  34. package/dist/cjs/fork/getStateFromPath.test.native.js +374 -0
  35. package/dist/cjs/fork/getStateFromPath.test.native.js.map +6 -0
  36. package/dist/cjs/render.cjs +1 -1
  37. package/dist/cjs/render.js +1 -1
  38. package/dist/cjs/render.js.map +1 -1
  39. package/dist/cjs/router/FlagsContext.cjs +27 -0
  40. package/dist/cjs/router/FlagsContext.js +22 -0
  41. package/dist/cjs/router/FlagsContext.js.map +6 -0
  42. package/dist/cjs/router/FlagsContext.native.js +26 -0
  43. package/dist/cjs/router/FlagsContext.native.js.map +6 -0
  44. package/dist/cjs/router/getRoutes.cjs +11 -1
  45. package/dist/cjs/router/getRoutes.js +11 -1
  46. package/dist/cjs/router/getRoutes.js.map +1 -1
  47. package/dist/cjs/router/getRoutes.native.js +11 -1
  48. package/dist/cjs/router/getRoutes.native.js.map +2 -2
  49. package/dist/cjs/router/matchers.test.cjs +38 -0
  50. package/dist/cjs/router/matchers.test.js +42 -0
  51. package/dist/cjs/router/matchers.test.js.map +6 -0
  52. package/dist/cjs/router/matchers.test.native.js +39 -0
  53. package/dist/cjs/router/matchers.test.native.js.map +6 -0
  54. package/dist/cjs/router/router.cjs +3 -35
  55. package/dist/cjs/router/router.js +2 -26
  56. package/dist/cjs/router/router.js.map +1 -1
  57. package/dist/cjs/router/router.native.js +2 -33
  58. package/dist/cjs/router/router.native.js.map +2 -2
  59. package/dist/cjs/router/utils/getNavigateAction.cjs +61 -0
  60. package/dist/cjs/router/utils/getNavigateAction.js +46 -0
  61. package/dist/cjs/router/utils/getNavigateAction.js.map +6 -0
  62. package/dist/cjs/router/utils/getNavigateAction.native.js +58 -0
  63. package/dist/cjs/router/utils/getNavigateAction.native.js.map +6 -0
  64. package/dist/cjs/router/utils/getNavigateAction.test.cjs +259 -0
  65. package/dist/cjs/router/utils/getNavigateAction.test.js +295 -0
  66. package/dist/cjs/router/utils/getNavigateAction.test.js.map +6 -0
  67. package/dist/cjs/router/utils/getNavigateAction.test.native.js +330 -0
  68. package/dist/cjs/router/utils/getNavigateAction.test.native.js.map +6 -0
  69. package/dist/cjs/testing-utils.cjs +63 -0
  70. package/dist/cjs/testing-utils.js +55 -0
  71. package/dist/cjs/testing-utils.js.map +6 -0
  72. package/dist/cjs/testing-utils.native.js +79 -0
  73. package/dist/cjs/testing-utils.native.js.map +6 -0
  74. package/dist/cjs/views/Navigator.cjs +8 -1
  75. package/dist/cjs/views/Navigator.js +25 -11
  76. package/dist/cjs/views/Navigator.js.map +1 -1
  77. package/dist/cjs/views/Navigator.native.js +7 -4
  78. package/dist/cjs/views/Navigator.native.js.map +2 -2
  79. package/dist/cjs/vite/one.cjs +6 -1
  80. package/dist/cjs/vite/one.js +6 -1
  81. package/dist/cjs/vite/one.js.map +1 -1
  82. package/dist/cjs/vite/one.native.js +7 -2
  83. package/dist/cjs/vite/one.native.js.map +2 -2
  84. package/dist/cjs/vite/plugins/virtualEntryPlugin.cjs +2 -0
  85. package/dist/cjs/vite/plugins/virtualEntryPlugin.js +2 -0
  86. package/dist/cjs/vite/plugins/virtualEntryPlugin.js.map +1 -1
  87. package/dist/cjs/vite/plugins/virtualEntryPlugin.native.js +2 -0
  88. package/dist/cjs/vite/plugins/virtualEntryPlugin.native.js.map +2 -2
  89. package/dist/esm/Root.js +2 -1
  90. package/dist/esm/Root.js.map +1 -1
  91. package/dist/esm/Root.mjs +5 -1
  92. package/dist/esm/Root.mjs.map +1 -1
  93. package/dist/esm/Root.native.js +5 -1
  94. package/dist/esm/Root.native.js.map +1 -1
  95. package/dist/esm/createApp.js +16 -2
  96. package/dist/esm/createApp.js.map +1 -1
  97. package/dist/esm/createApp.mjs +2 -0
  98. package/dist/esm/createApp.mjs.map +1 -1
  99. package/dist/esm/createApp.native.js +1 -0
  100. package/dist/esm/createApp.native.js.map +1 -1
  101. package/dist/esm/fork/__tests__/getPathFromState.test.js +1561 -0
  102. package/dist/esm/fork/__tests__/getPathFromState.test.js.map +6 -0
  103. package/dist/esm/fork/__tests__/getPathFromState.test.mjs +1441 -0
  104. package/dist/esm/fork/__tests__/getPathFromState.test.mjs.map +1 -0
  105. package/dist/esm/fork/__tests__/getPathFromState.test.native.js +1580 -0
  106. package/dist/esm/fork/__tests__/getPathFromState.test.native.js.map +1 -0
  107. package/dist/esm/fork/__tests__/getStateFromPath.test.js +2706 -0
  108. package/dist/esm/fork/__tests__/getStateFromPath.test.js.map +6 -0
  109. package/dist/esm/fork/__tests__/getStateFromPath.test.mjs +2566 -0
  110. package/dist/esm/fork/__tests__/getStateFromPath.test.mjs.map +1 -0
  111. package/dist/esm/fork/__tests__/getStateFromPath.test.native.js +2636 -0
  112. package/dist/esm/fork/__tests__/getStateFromPath.test.native.js.map +1 -0
  113. package/dist/esm/fork/getPathFromState.js +1 -1
  114. package/dist/esm/fork/getPathFromState.js.map +1 -1
  115. package/dist/esm/fork/getPathFromState.mjs +2 -1
  116. package/dist/esm/fork/getPathFromState.mjs.map +1 -1
  117. package/dist/esm/fork/getPathFromState.native.js +9 -5
  118. package/dist/esm/fork/getPathFromState.native.js.map +1 -1
  119. package/dist/esm/fork/getPathFromState.test.js +123 -0
  120. package/dist/esm/fork/getPathFromState.test.js.map +6 -0
  121. package/dist/esm/fork/getPathFromState.test.mjs +114 -0
  122. package/dist/esm/fork/getPathFromState.test.mjs.map +1 -0
  123. package/dist/esm/fork/getPathFromState.test.native.js +122 -0
  124. package/dist/esm/fork/getPathFromState.test.native.js.map +1 -0
  125. package/dist/esm/fork/getStateFromPath.test.js +294 -0
  126. package/dist/esm/fork/getStateFromPath.test.js.map +6 -0
  127. package/dist/esm/fork/getStateFromPath.test.mjs +230 -0
  128. package/dist/esm/fork/getStateFromPath.test.mjs.map +1 -0
  129. package/dist/esm/fork/getStateFromPath.test.native.js +233 -0
  130. package/dist/esm/fork/getStateFromPath.test.native.js.map +1 -0
  131. package/dist/esm/render.js +1 -1
  132. package/dist/esm/render.js.map +1 -1
  133. package/dist/esm/render.mjs +1 -1
  134. package/dist/esm/render.mjs.map +1 -1
  135. package/dist/esm/router/FlagsContext.js +6 -0
  136. package/dist/esm/router/FlagsContext.js.map +6 -0
  137. package/dist/esm/router/FlagsContext.mjs +4 -0
  138. package/dist/esm/router/FlagsContext.mjs.map +1 -0
  139. package/dist/esm/router/FlagsContext.native.js +4 -0
  140. package/dist/esm/router/FlagsContext.native.js.map +1 -0
  141. package/dist/esm/router/getRoutes.js +11 -1
  142. package/dist/esm/router/getRoutes.js.map +1 -1
  143. package/dist/esm/router/getRoutes.mjs +11 -1
  144. package/dist/esm/router/getRoutes.mjs.map +1 -1
  145. package/dist/esm/router/getRoutes.native.js +11 -1
  146. package/dist/esm/router/getRoutes.native.js.map +1 -1
  147. package/dist/esm/router/matchers.test.js +50 -0
  148. package/dist/esm/router/matchers.test.js.map +6 -0
  149. package/dist/esm/router/matchers.test.mjs +39 -0
  150. package/dist/esm/router/matchers.test.mjs.map +1 -0
  151. package/dist/esm/router/matchers.test.native.js +39 -0
  152. package/dist/esm/router/matchers.test.native.js.map +1 -0
  153. package/dist/esm/router/router.js +1 -26
  154. package/dist/esm/router/router.js.map +1 -1
  155. package/dist/esm/router/router.mjs +1 -33
  156. package/dist/esm/router/router.mjs.map +1 -1
  157. package/dist/esm/router/router.native.js +1 -37
  158. package/dist/esm/router/router.native.js.map +1 -1
  159. package/dist/esm/router/utils/getNavigateAction.js +32 -0
  160. package/dist/esm/router/utils/getNavigateAction.js.map +6 -0
  161. package/dist/esm/router/utils/getNavigateAction.mjs +38 -0
  162. package/dist/esm/router/utils/getNavigateAction.mjs.map +1 -0
  163. package/dist/esm/router/utils/getNavigateAction.native.js +42 -0
  164. package/dist/esm/router/utils/getNavigateAction.native.js.map +1 -0
  165. package/dist/esm/router/utils/getNavigateAction.test.js +296 -0
  166. package/dist/esm/router/utils/getNavigateAction.test.js.map +6 -0
  167. package/dist/esm/router/utils/getNavigateAction.test.mjs +260 -0
  168. package/dist/esm/router/utils/getNavigateAction.test.mjs.map +1 -0
  169. package/dist/esm/router/utils/getNavigateAction.test.native.js +273 -0
  170. package/dist/esm/router/utils/getNavigateAction.test.native.js.map +1 -0
  171. package/dist/esm/testing-utils.js +33 -0
  172. package/dist/esm/testing-utils.js.map +6 -0
  173. package/dist/esm/testing-utils.mjs +27 -0
  174. package/dist/esm/testing-utils.mjs.map +1 -0
  175. package/dist/esm/testing-utils.native.js +38 -0
  176. package/dist/esm/testing-utils.native.js.map +1 -0
  177. package/dist/esm/views/Navigator.js +29 -11
  178. package/dist/esm/views/Navigator.js.map +1 -1
  179. package/dist/esm/views/Navigator.mjs +8 -1
  180. package/dist/esm/views/Navigator.mjs.map +1 -1
  181. package/dist/esm/views/Navigator.native.js +8 -2
  182. package/dist/esm/views/Navigator.native.js.map +1 -1
  183. package/dist/esm/vite/one.js +6 -1
  184. package/dist/esm/vite/one.js.map +1 -1
  185. package/dist/esm/vite/one.mjs +6 -1
  186. package/dist/esm/vite/one.mjs.map +1 -1
  187. package/dist/esm/vite/one.native.js +9 -2
  188. package/dist/esm/vite/one.native.js.map +1 -1
  189. package/dist/esm/vite/plugins/virtualEntryPlugin.js +2 -0
  190. package/dist/esm/vite/plugins/virtualEntryPlugin.js.map +1 -1
  191. package/dist/esm/vite/plugins/virtualEntryPlugin.mjs +2 -0
  192. package/dist/esm/vite/plugins/virtualEntryPlugin.mjs.map +1 -1
  193. package/dist/esm/vite/plugins/virtualEntryPlugin.native.js +2 -0
  194. package/dist/esm/vite/plugins/virtualEntryPlugin.native.js.map +1 -1
  195. package/package.json +13 -10
  196. package/src/Root.tsx +12 -2
  197. package/src/createApp.native.tsx +8 -2
  198. package/src/createApp.tsx +18 -3
  199. package/src/fork/__tests__/README.md +8 -0
  200. package/src/fork/__tests__/getPathFromState.test.tsx +1809 -0
  201. package/src/fork/__tests__/getStateFromPath.test.tsx +3188 -0
  202. package/src/fork/getPathFromState.test.ts +146 -0
  203. package/src/fork/getPathFromState.ts +1 -1
  204. package/src/fork/getStateFromPath.test.ts +345 -0
  205. package/src/render.tsx +3 -3
  206. package/src/router/FlagsContext.ts +4 -0
  207. package/src/router/getRoutes.ts +14 -2
  208. package/src/router/matchers.test.ts +120 -0
  209. package/src/router/router.ts +1 -113
  210. package/src/router/utils/getNavigateAction.test.ts +334 -0
  211. package/src/router/utils/getNavigateAction.ts +120 -0
  212. package/src/testing-utils.ts +56 -0
  213. package/src/views/Navigator.tsx +34 -10
  214. package/src/vite/one.ts +5 -0
  215. package/src/vite/plugins/virtualEntryPlugin.ts +4 -1
  216. package/src/vite/types.ts +18 -0
  217. package/types/Root.d.ts +1 -0
  218. package/types/Root.d.ts.map +1 -1
  219. package/types/createApp.d.ts +2 -0
  220. package/types/createApp.d.ts.map +1 -1
  221. package/types/createApp.native.d.ts +2 -0
  222. package/types/createApp.native.d.ts.map +1 -1
  223. package/types/fork/getPathFromState.test.d.ts +2 -0
  224. package/types/fork/getPathFromState.test.d.ts.map +1 -0
  225. package/types/fork/getStateFromPath.test.d.ts +2 -0
  226. package/types/fork/getStateFromPath.test.d.ts.map +1 -0
  227. package/types/router/FlagsContext.d.ts +3 -0
  228. package/types/router/FlagsContext.d.ts.map +1 -0
  229. package/types/router/getRoutes.d.ts.map +1 -1
  230. package/types/router/matchers.test.d.ts +2 -0
  231. package/types/router/matchers.test.d.ts.map +1 -0
  232. package/types/router/router.d.ts.map +1 -1
  233. package/types/router/utils/getNavigateAction.d.ts +17 -0
  234. package/types/router/utils/getNavigateAction.d.ts.map +1 -0
  235. package/types/router/utils/getNavigateAction.test.d.ts +2 -0
  236. package/types/router/utils/getNavigateAction.test.d.ts.map +1 -0
  237. package/types/testing-utils.d.ts +26 -0
  238. package/types/testing-utils.d.ts.map +1 -0
  239. package/types/views/Navigator.d.ts +1 -1
  240. package/types/views/Navigator.d.ts.map +1 -1
  241. package/types/vite/one.d.ts.map +1 -1
  242. package/types/vite/plugins/virtualEntryPlugin.d.ts +2 -0
  243. package/types/vite/plugins/virtualEntryPlugin.d.ts.map +1 -1
  244. package/types/vite/types.d.ts +16 -0
  245. package/types/vite/types.d.ts.map +1 -1
@@ -1,11 +1,9 @@
1
- import type { NavigationState, PartialRoute } from '@react-navigation/native'
2
1
  import {
3
2
  StackActions,
4
3
  type NavigationContainerRefWithCurrent,
5
4
  type getPathFromState as originalGetPathFromState,
6
5
  } from '@react-navigation/native'
7
6
  import * as Linking from 'expo-linking'
8
- import { nanoid } from 'nanoid/non-secure'
9
7
  import { Fragment, startTransition, type ComponentType, useSyncExternalStore } from 'react'
10
8
  import { Platform } from 'react-native'
11
9
  import type { State } from '../fork/getPathFromState'
@@ -23,10 +21,10 @@ import { getLinkingConfig, type OneLinkingOptions } from './getLinkingConfig'
23
21
  import { getNormalizedStatePath, type UrlObject } from './getNormalizedStatePath'
24
22
  import { getRoutes } from './getRoutes'
25
23
  import { setLastAction } from './lastAction'
26
- import { matchDynamicName } from './matchers'
27
24
  import type { RouteNode } from './Route'
28
25
  import { sortRoutes } from './sortRoutes'
29
26
  import { getQualifiedRouteComponent } from './useScreens'
27
+ import { getNavigateAction } from './utils/getNavigateAction'
30
28
 
31
29
  // Module-scoped variables
32
30
  export let routeNode: RouteNode | null = null
@@ -549,116 +547,6 @@ const hashes: Record<string, string> = {}
549
547
 
550
548
  let nextOptions: OneRouter.LinkToOptions | null = null
551
549
 
552
- function getNavigateAction(
553
- actionState: OneRouter.ResultState,
554
- navigationState: NavigationState,
555
- type = 'NAVIGATE'
556
- ) {
557
- /**
558
- * We need to find the deepest navigator where the action and current state diverge, If they do not diverge, the
559
- * lowest navigator is the target.
560
- *
561
- * By default React Navigation will target the current navigator, but this doesn't work for all actions
562
- * For example:
563
- * - /deeply/nested/route -> /top-level-route the target needs to be the top-level navigator
564
- * - /stack/nestedStack/page -> /stack1/nestedStack/other-page needs to target the nestedStack navigator
565
- *
566
- * This matching needs to done by comparing the route names and the dynamic path, for example
567
- * - /1/page -> /2/anotherPage needs to target the /[id] navigator
568
- *
569
- * Other parameters such as search params and hash are not evaluated.
570
- *
571
- */
572
- let actionStateRoute: PartialRoute<any> | undefined
573
-
574
- // Traverse the state tree comparing the current state and the action state until we find where they diverge
575
- while (actionState && navigationState) {
576
- const stateRoute = navigationState.routes[navigationState.index]
577
-
578
- actionStateRoute = actionState.routes[actionState.routes.length - 1]
579
-
580
- const childState = actionStateRoute.state
581
- const nextNavigationState = stateRoute.state
582
-
583
- const dynamicName = matchDynamicName(actionStateRoute.name)
584
-
585
- const didActionAndCurrentStateDiverge =
586
- actionStateRoute.name !== stateRoute.name ||
587
- !childState ||
588
- !nextNavigationState ||
589
- (dynamicName && actionStateRoute.params?.[dynamicName] !== stateRoute.params?.[dynamicName])
590
-
591
- if (didActionAndCurrentStateDiverge) {
592
- break
593
- }
594
-
595
- actionState = childState
596
- navigationState = nextNavigationState as NavigationState
597
- }
598
-
599
- /*
600
- * We found the target navigator, but the payload is in the incorrect format
601
- * We need to convert the action state to a payload that can be dispatched
602
- */
603
- const rootPayload: Record<string, any> = { params: {} }
604
- let payload = rootPayload
605
- let params = payload.params
606
-
607
- // The root level of payload is a bit weird, its params are in the child object
608
- while (actionStateRoute) {
609
- Object.assign(params, { ...actionStateRoute.params })
610
- payload.screen = actionStateRoute.name
611
- payload.params = { ...actionStateRoute.params }
612
-
613
- actionStateRoute = actionStateRoute.state?.routes[actionStateRoute.state?.routes.length - 1]
614
-
615
- payload.params ??= {}
616
- payload = payload.params
617
- params = payload
618
- }
619
-
620
- // One uses only three actions, but these don't directly translate to all navigator actions
621
- if (type === 'PUSH') {
622
- setLastAction()
623
-
624
- // Only stack navigators have a push action, and even then we want to use NAVIGATE (see below)
625
- type = 'NAVIGATE'
626
-
627
- /*
628
- * The StackAction.PUSH does not work correctly with One.
629
- *
630
- * One provides a getId() function for every route, altering how React Navigation handles stack routing.
631
- * Ordinarily, PUSH always adds a new screen to the stack. However, with getId() present, it navigates to the screen with the matching ID instead
632
- * (by moving the screen to the top of the stack)
633
- * When you try and push to a screen with the same ID, no navigation will occur
634
- * Refer to: https://github.com/react-navigation/react-navigation/blob/13d4aa270b301faf07960b4cd861ffc91e9b2c46/packages/routers/src/StackRouter.tsx#L279-L290
635
- *
636
- * One needs to retain the default behavior of PUSH, consistently adding new screens to the stack, even if their IDs are identical.
637
- *
638
- * To resolve this issue, we switch to using a NAVIGATE action with a new key. In the navigate action, screens are matched by either key or getId() function.
639
- * By generating a unique new key, we ensure that the screen is always pushed onto the stack.
640
- *
641
- */
642
- if (navigationState.type === 'stack') {
643
- rootPayload.key = `${rootPayload.name}-${nanoid()}` // @see https://github.com/react-navigation/react-navigation/blob/13d4aa270b301faf07960b4cd861ffc91e9b2c46/packages/routers/src/StackRouter.tsx#L406-L407
644
- }
645
- }
646
-
647
- if (type === 'REPLACE' && navigationState.type === 'tab') {
648
- type = 'JUMP_TO'
649
- }
650
-
651
- return {
652
- type,
653
- target: navigationState.key,
654
- payload: {
655
- key: rootPayload.key,
656
- name: rootPayload.screen,
657
- params: rootPayload.params,
658
- },
659
- }
660
- }
661
-
662
550
  function deepEqual(a: any, b: any) {
663
551
  if (a === b) {
664
552
  return true
@@ -0,0 +1,334 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { getNavigateAction } from './getNavigateAction'
3
+
4
+ describe('getNavigateAction', () => {
5
+ describe('NAVIGATE', () => {
6
+ it('works', () => {
7
+ const actionState = {
8
+ routes: [
9
+ {
10
+ name: 'page-2',
11
+ },
12
+ ],
13
+ }
14
+
15
+ const navigationState = {
16
+ stale: false as const,
17
+ type: 'stack',
18
+ key: 'stack-pWRo04',
19
+ index: 0,
20
+ routeNames: ['_sitemap', 'index', 'page-1', 'page-2'],
21
+ routes: [
22
+ {
23
+ name: 'page-1',
24
+ key: 'page-1-Gc-TeIdZmx_jAcRD-SGcs',
25
+ },
26
+ ],
27
+ preloadedRoutes: [],
28
+ }
29
+
30
+ const action = getNavigateAction(actionState, navigationState)
31
+
32
+ expect(action).toStrictEqual({
33
+ type: 'NAVIGATE',
34
+ target: 'stack-pWRo04',
35
+ payload: { key: undefined, name: 'page-2', params: {} },
36
+ })
37
+ })
38
+
39
+ it('handles params', () => {
40
+ const actionState = {
41
+ routes: [
42
+ {
43
+ name: 'page-1',
44
+ params: {
45
+ foo: 'foo',
46
+ bar: 'bar',
47
+ },
48
+ },
49
+ ],
50
+ }
51
+
52
+ const navigationState = {
53
+ stale: false as const,
54
+ type: 'stack',
55
+ key: 'stack-pWRo04',
56
+ index: 0,
57
+ routeNames: ['_sitemap', 'index', 'page-1', 'page-2'],
58
+ routes: [
59
+ {
60
+ name: 'page-1',
61
+ key: 'page-1-Gc-TeIdZmx_jAcRD-SGcs',
62
+ },
63
+ ],
64
+ preloadedRoutes: [],
65
+ }
66
+
67
+ const action = getNavigateAction(actionState, navigationState)
68
+
69
+ expect(action).toStrictEqual({
70
+ type: 'NAVIGATE',
71
+ target: 'stack-pWRo04',
72
+ payload: {
73
+ key: undefined,
74
+ name: 'page-1',
75
+ params: {
76
+ foo: 'foo',
77
+ bar: 'bar',
78
+ },
79
+ },
80
+ })
81
+ })
82
+
83
+ it('handles navigating into nested navigator', () => {
84
+ const actionState = {
85
+ routes: [
86
+ {
87
+ name: 'foo',
88
+ state: {
89
+ routes: [
90
+ {
91
+ name: 'bar',
92
+ state: {
93
+ routes: [
94
+ {
95
+ name: 'baz',
96
+ },
97
+ ],
98
+ },
99
+ },
100
+ ],
101
+ },
102
+ },
103
+ ],
104
+ }
105
+
106
+ const navigationState = {
107
+ stale: false as const,
108
+ type: 'stack',
109
+ key: 'stack-5qQ9ln4FB9',
110
+ index: 0,
111
+ routeNames: ['index', 'foo'],
112
+ routes: [
113
+ {
114
+ name: 'index',
115
+ key: 'index-Kyz4PdQ7ZAvE0XFhBWydM',
116
+ },
117
+ ],
118
+ preloadedRoutes: [],
119
+ }
120
+
121
+ const action = getNavigateAction(actionState, navigationState)
122
+
123
+ expect(action).toStrictEqual({
124
+ type: 'NAVIGATE',
125
+ target: 'stack-5qQ9ln4FB9',
126
+ payload: {
127
+ key: undefined,
128
+ name: 'foo',
129
+ params: {
130
+ screen: 'bar',
131
+ params: {
132
+ screen: 'baz',
133
+ params: {},
134
+ },
135
+ },
136
+ },
137
+ })
138
+ })
139
+
140
+ it('handles navigating into nested navigator with route params', () => {
141
+ const actionState = {
142
+ routes: [
143
+ {
144
+ name: '[level1]',
145
+ params: {
146
+ level1: 'foo',
147
+ },
148
+ state: {
149
+ routes: [
150
+ {
151
+ name: '[level2]',
152
+ params: {
153
+ level1: 'foo',
154
+ level2: 'bar',
155
+ },
156
+ state: {
157
+ routes: [
158
+ {
159
+ name: '[level3]',
160
+ params: {
161
+ level1: 'foo',
162
+ level2: 'bar',
163
+ level3: 'baz',
164
+ },
165
+ },
166
+ ],
167
+ },
168
+ },
169
+ ],
170
+ },
171
+ },
172
+ ],
173
+ }
174
+
175
+ const navigationState = {
176
+ stale: false as const,
177
+ type: 'stack',
178
+ key: 'stack-5qQ9ln4FB9',
179
+ index: 0,
180
+ routeNames: ['index', '[level1]'],
181
+ routes: [
182
+ {
183
+ name: 'index',
184
+ key: 'index-Kyz4PdQ7ZAvE0XFhBWydM',
185
+ },
186
+ ],
187
+ preloadedRoutes: [],
188
+ }
189
+
190
+ const action = getNavigateAction(actionState, navigationState)
191
+
192
+ expect(action).toStrictEqual({
193
+ type: 'NAVIGATE',
194
+ target: 'stack-5qQ9ln4FB9',
195
+ payload: {
196
+ key: undefined,
197
+ name: '[level1]',
198
+ params: {
199
+ level1: 'foo',
200
+ level2: 'bar', // not actually necessary, but it's how the current implementation works
201
+ screen: '[level2]',
202
+ params: {
203
+ level1: 'foo',
204
+ level2: 'bar',
205
+ level3: 'baz', // not actually necessary, but it's how the current implementation works
206
+ screen: '[level3]',
207
+ params: {
208
+ level1: 'foo',
209
+ level2: 'bar',
210
+ level3: 'baz',
211
+ },
212
+ },
213
+ },
214
+ },
215
+ })
216
+ })
217
+
218
+ it('correctly finds out where the states diverge and return an action valid payload', () => {
219
+ const actionState = {
220
+ routes: [
221
+ {
222
+ name: 'foo',
223
+ state: {
224
+ routes: [
225
+ {
226
+ name: 'bar',
227
+ state: {
228
+ routes: [
229
+ {
230
+ name: 'baz-2',
231
+ },
232
+ ],
233
+ },
234
+ },
235
+ ],
236
+ },
237
+ },
238
+ ],
239
+ }
240
+
241
+ const navigationState = {
242
+ stale: false as const,
243
+ type: 'stack',
244
+ key: 'stack-aCzOliK0',
245
+ routeNames: ['index', 'foo'],
246
+ index: 0,
247
+ routes: [
248
+ {
249
+ name: 'foo',
250
+ key: 'foo-teuUBQHk',
251
+ state: {
252
+ stale: false as const,
253
+ type: 'stack',
254
+ key: 'stack-XZ3RJRBg',
255
+ routeNames: ['bar'],
256
+ index: 0,
257
+ routes: [
258
+ {
259
+ name: 'bar',
260
+ key: 'bar-FDtH59Dj',
261
+ state: {
262
+ stale: false as const,
263
+ type: 'stack',
264
+ key: 'stack-s3o7RyPD',
265
+ routeNames: ['baz-1', 'baz-2'],
266
+ index: 0,
267
+ routes: [
268
+ {
269
+ name: 'baz-1',
270
+ key: 'baz-1-K2zLhRSZ',
271
+ },
272
+ ],
273
+ },
274
+ },
275
+ ],
276
+ },
277
+ },
278
+ ],
279
+ preloadedRoutes: [],
280
+ }
281
+
282
+ const action = getNavigateAction(actionState, navigationState)
283
+
284
+ expect(action).toStrictEqual({
285
+ type: 'NAVIGATE',
286
+ target: 'stack-s3o7RyPD',
287
+ payload: {
288
+ key: undefined,
289
+ name: 'baz-2',
290
+ params: {},
291
+ },
292
+ })
293
+ })
294
+ })
295
+
296
+ describe('PUSH', () => {
297
+ it('returns a NAVIGATE action with a unique key', () => {
298
+ const actionState = {
299
+ routes: [
300
+ {
301
+ name: 'page-2',
302
+ },
303
+ ],
304
+ }
305
+
306
+ const navigationState = {
307
+ stale: false as const,
308
+ type: 'stack',
309
+ key: 'stack-pWRo04',
310
+ index: 0,
311
+ routeNames: ['_sitemap', 'index', 'page-1', 'page-2'],
312
+ routes: [
313
+ {
314
+ name: 'page-1',
315
+ key: 'page-1-Gc-TeIdZmx_jAcRD-SGcs',
316
+ },
317
+ ],
318
+ preloadedRoutes: [],
319
+ }
320
+
321
+ const action = getNavigateAction(actionState, navigationState, 'PUSH')
322
+
323
+ expect(action).toStrictEqual({
324
+ type: 'NAVIGATE',
325
+ target: 'stack-pWRo04',
326
+ payload: {
327
+ key: action.payload.key /* since this contains a randomly generated nanoid */,
328
+ name: 'page-2',
329
+ params: {},
330
+ },
331
+ })
332
+ })
333
+ })
334
+ })
@@ -0,0 +1,120 @@
1
+ import { nanoid } from 'nanoid'
2
+ import type { NavigationState, PartialRoute } from '@react-navigation/core'
3
+ import type { OneRouter } from '../../interfaces/router'
4
+ import { matchDynamicName } from '../matchers'
5
+ import { setLastAction } from '../lastAction'
6
+
7
+ /**
8
+ * Generates a navigation action to transition from the current state to the desired state.
9
+ */
10
+ export function getNavigateAction(
11
+ /** desired state */
12
+ actionState: OneRouter.ResultState,
13
+ navigationState: NavigationState,
14
+ type = 'NAVIGATE'
15
+ ) {
16
+ /**
17
+ * We need to find the deepest navigator where the action and current state diverge, If they do not diverge, the
18
+ * lowest navigator is the target.
19
+ *
20
+ * By default React Navigation will target the current navigator, but this doesn't work for all actions
21
+ * For example:
22
+ * - /deeply/nested/route -> /top-level-route the target needs to be the top-level navigator
23
+ * - /stack/nestedStack/page -> /stack1/nestedStack/other-page needs to target the nestedStack navigator
24
+ *
25
+ * This matching needs to done by comparing the route names and the dynamic path, for example
26
+ * - /1/page -> /2/anotherPage needs to target the /[id] navigator
27
+ *
28
+ * Other parameters such as search params and hash are not evaluated.
29
+ *
30
+ */
31
+ let actionStateRoute: PartialRoute<any> | undefined
32
+
33
+ // Traverse the state tree comparing the current state and the action state until we find where they diverge
34
+ while (actionState && navigationState) {
35
+ const stateRoute = navigationState.routes[navigationState.index]
36
+
37
+ actionStateRoute = actionState.routes[actionState.routes.length - 1]
38
+
39
+ const childState = actionStateRoute.state
40
+ const nextNavigationState = stateRoute.state
41
+
42
+ const dynamicName = matchDynamicName(actionStateRoute.name)
43
+
44
+ const didActionAndCurrentStateDiverge =
45
+ actionStateRoute.name !== stateRoute.name ||
46
+ // !deepEqual(actionStateRoute.params, stateRoute.params) ||
47
+ !childState ||
48
+ !nextNavigationState ||
49
+ (dynamicName && actionStateRoute.params?.[dynamicName] !== stateRoute.params?.[dynamicName])
50
+
51
+ if (didActionAndCurrentStateDiverge) {
52
+ break
53
+ }
54
+
55
+ actionState = childState
56
+ navigationState = nextNavigationState as NavigationState
57
+ }
58
+
59
+ /*
60
+ * We found the target navigator, but the payload is in the incorrect format
61
+ * We need to convert the action state to a payload that can be dispatched
62
+ */
63
+ const rootPayload: Record<string, any> = { params: {} }
64
+ let payload = rootPayload
65
+ let params = payload.params
66
+
67
+ // The root level of payload is a bit weird, its params are in the child object
68
+ while (actionStateRoute) {
69
+ Object.assign(params, { ...actionStateRoute.params })
70
+ payload.screen = actionStateRoute.name
71
+ payload.params = { ...actionStateRoute.params }
72
+
73
+ actionStateRoute = actionStateRoute.state?.routes[actionStateRoute.state?.routes.length - 1]
74
+
75
+ payload.params ??= {}
76
+ payload = payload.params
77
+ params = payload
78
+ }
79
+
80
+ // One uses only three actions, but these don't directly translate to all navigator actions
81
+ if (type === 'PUSH') {
82
+ setLastAction()
83
+
84
+ // Only stack navigators have a push action, and even then we want to use NAVIGATE (see below)
85
+ type = 'NAVIGATE'
86
+
87
+ /*
88
+ * The StackAction.PUSH does not work correctly with One.
89
+ *
90
+ * One provides a getId() function for every route, altering how React Navigation handles stack routing.
91
+ * Ordinarily, PUSH always adds a new screen to the stack. However, with getId() present, it navigates to the screen with the matching ID instead
92
+ * (by moving the screen to the top of the stack)
93
+ * When you try and push to a screen with the same ID, no navigation will occur
94
+ * Refer to: https://github.com/react-navigation/react-navigation/blob/13d4aa270b301faf07960b4cd861ffc91e9b2c46/packages/routers/src/StackRouter.tsx#L279-L290
95
+ *
96
+ * One needs to retain the default behavior of PUSH, consistently adding new screens to the stack, even if their IDs are identical.
97
+ *
98
+ * To resolve this issue, we switch to using a NAVIGATE action with a new key. In the navigate action, screens are matched by either key or getId() function.
99
+ * By generating a unique new key, we ensure that the screen is always pushed onto the stack.
100
+ *
101
+ */
102
+ if (navigationState.type === 'stack') {
103
+ rootPayload.key = `${rootPayload.name}-${nanoid()}` // @see https://github.com/react-navigation/react-navigation/blob/13d4aa270b301faf07960b4cd861ffc91e9b2c46/packages/routers/src/StackRouter.tsx#L406-L407
104
+ }
105
+ }
106
+
107
+ if (type === 'REPLACE' && navigationState.type === 'tab') {
108
+ type = 'JUMP_TO'
109
+ }
110
+
111
+ return {
112
+ type,
113
+ target: navigationState.key,
114
+ payload: {
115
+ key: rootPayload.key,
116
+ name: rootPayload.screen,
117
+ params: rootPayload.params,
118
+ },
119
+ }
120
+ }
@@ -0,0 +1,56 @@
1
+ import path from 'node:path'
2
+
3
+ import { getRoutes } from './router/getRoutes'
4
+ import { getReactNavigationConfig } from './getReactNavigationConfig'
5
+
6
+ export type ReactComponent = () => React.ReactElement<any, any> | null
7
+ export type FileStub =
8
+ | (Record<string, unknown> & {
9
+ default: ReactComponent
10
+ unstable_settings?: Record<string, any>
11
+ })
12
+ | ReactComponent
13
+ export type NativeIntentStub = any
14
+ export type MemoryContext = Record<string, FileStub | NativeIntentStub> & {
15
+ '+native-intent'?: NativeIntentStub
16
+ }
17
+
18
+ const validExtensions = ['.js', '.jsx', '.ts', '.tsx']
19
+
20
+ export function inMemoryContext(context: MemoryContext) {
21
+ return Object.assign(
22
+ (id: string) => {
23
+ id = id.replace(/^\.\//, '').replace(/\.\w*$/, '')
24
+ return typeof context[id] === 'function' ? { default: context[id] } : context[id]
25
+ },
26
+ {
27
+ resolve: (key: string) => key,
28
+ id: '0',
29
+ keys: () =>
30
+ Object.keys(context).map((key) => {
31
+ const ext = path.extname(key)
32
+ key = key.replace(/^\.\//, '')
33
+ key = key.startsWith('/') ? key : `./${key}`
34
+ key = validExtensions.includes(ext) ? key : `${key}.js`
35
+
36
+ return key
37
+ }),
38
+ }
39
+ )
40
+ }
41
+
42
+ type MockContextConfig = string[] // Array of filenames to mock as empty components, e.g () => null
43
+
44
+ export function getMockContext(context: MockContextConfig) {
45
+ if (Array.isArray(context)) {
46
+ return inMemoryContext(
47
+ Object.fromEntries(context.map((filename) => [filename, { default: () => null }]))
48
+ )
49
+ }
50
+
51
+ throw new Error('Invalid context')
52
+ }
53
+
54
+ export function getMockConfig(context: MockContextConfig, metaOnly = true) {
55
+ return getReactNavigationConfig(getRoutes(getMockContext(context))!, metaOnly)
56
+ }