@apexnative/navigation 1.0.0-alpha.6 → 1.0.0-alpha.7

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 (236) hide show
  1. package/ApexNavigation.podspec +2 -1
  2. package/CHANGELOG.md +7 -0
  3. package/android/CLAUDE.md +19 -10
  4. package/android/CORE.md +3 -2
  5. package/android/consumer-rules.pro +6 -0
  6. package/android/src/main/java/dev/apexnative/navigation/options/BottomTabOptions.kt +1 -1
  7. package/android/src/main/java/dev/apexnative/navigation/options/ButtonOptions.kt +1 -1
  8. package/android/src/main/java/dev/apexnative/navigation/options/LargeTitleOptions.kt +1 -1
  9. package/android/src/main/java/dev/apexnative/navigation/options/LayoutAnimation.kt +1 -1
  10. package/android/src/main/java/dev/apexnative/navigation/options/OptionKey.kt +18 -0
  11. package/android/src/main/java/dev/apexnative/navigation/options/OptionPaths.kt +91 -0
  12. package/android/src/main/java/dev/apexnative/navigation/options/SheetOptions.kt +7 -4
  13. package/android/src/main/java/dev/apexnative/navigation/options/TitleOptions.kt +2 -2
  14. package/android/src/main/java/dev/apexnative/navigation/options/TopBarAppearanceOptions.kt +1 -1
  15. package/android/src/main/java/dev/apexnative/navigation/options/TopBarOptions.kt +16 -7
  16. package/android/src/main/java/dev/apexnative/navigation/react/ContentHeights.kt +95 -0
  17. package/android/src/main/java/dev/apexnative/navigation/react/ContentMounts.kt +66 -0
  18. package/android/src/main/java/dev/apexnative/navigation/react/NavigationTurboModule.kt +4 -0
  19. package/android/src/main/java/dev/apexnative/navigation/react/OptionsCommands.kt +41 -3
  20. package/android/src/main/java/dev/apexnative/navigation/react/ReactView.kt +16 -1
  21. package/android/src/main/java/dev/apexnative/navigation/slots/SlotScreenView.kt +27 -7
  22. package/android/src/main/java/dev/apexnative/navigation/slots/SlotView.kt +12 -1
  23. package/android/src/main/java/dev/apexnative/navigation/utils/MaterialTheme.kt +5 -6
  24. package/android/src/main/java/dev/apexnative/navigation/utils/SystemAnimations.kt +36 -0
  25. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalSheet.kt +329 -55
  26. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalViews.kt +26 -5
  27. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/SheetStops.kt +60 -2
  28. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuMotion.kt +8 -2
  29. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/BackPreview.kt +32 -4
  30. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackController.kt +46 -7
  31. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackQueue.kt +15 -3
  32. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackViews.kt +7 -4
  33. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/fab/FabMotion.kt +142 -0
  34. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/fab/FabPresenter.kt +215 -43
  35. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/search/StackSearchPresenter.kt +25 -26
  36. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/BarRowTransition.kt +3 -2
  37. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/ButtonPlaces.kt +11 -5
  38. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/TopBarPresenter.kt +36 -38
  39. package/android/src/main/java/dev/apexnative/navigation/views/element/ElementTransitions.kt +124 -181
  40. package/android/src/main/java/dev/apexnative/navigation/views/element/TargetsWait.kt +8 -1
  41. package/android/src/main/java/dev/apexnative/navigation/views/element/TransitionLayer.kt +37 -112
  42. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ElementPictures.kt +56 -3
  43. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/FlightGeometry.kt +170 -153
  44. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ImageMorph.kt +199 -0
  45. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/SharedAnimatorFactory.kt +55 -45
  46. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/{TextChangeAnimator.kt → TextMorph.kt} +79 -129
  47. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ViewMorph.kt +108 -0
  48. package/android/src/main/java/dev/apexnative/navigation/views/stack/fab/FabCopy.kt +85 -0
  49. package/android/src/main/java/dev/apexnative/navigation/views/stack/fab/FabMenu.kt +13 -0
  50. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/TitleBar.kt +5 -24
  51. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/TopBar.kt +51 -34
  52. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/titlebar/BarButtonComponentCell.kt +6 -6
  53. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/titlebar/BarButtonView.kt +113 -0
  54. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/titlebar/BarMenu.kt +219 -0
  55. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/titlebar/ButtonBar.kt +121 -175
  56. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/titlebar/ButtonMenus.kt +34 -64
  57. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/titlebar/ButtonRow.kt +216 -0
  58. package/android/src/main/jni/ANNContentHeights.cpp +82 -0
  59. package/android/src/main/res/drawable/apex_navigation_ic_arrow_right.xml +14 -0
  60. package/android/src/main/res/layout/apex_navigation_menu_header.xml +19 -0
  61. package/android/src/main/res/layout/apex_navigation_menu_item.xml +68 -0
  62. package/android/src/main/res/values/styles.xml +1 -1
  63. package/android/src/main/res/values-night/styles.xml +1 -1
  64. package/cli/doctor/checks/app.js +16 -1
  65. package/cli/project.js +4 -0
  66. package/cli/setup/index.js +5 -0
  67. package/cli/setup/steps/metro.js +46 -0
  68. package/docs-md/architecture/native-screens.md +4 -0
  69. package/docs-md/architecture/theme-and-context.md +131 -12
  70. package/docs-md/architecture/transitions.md +43 -4
  71. package/docs-md/flows/bottom-sheets.md +4 -2
  72. package/docs-md/fundamentals/screens-and-props.md +9 -1
  73. package/docs-md/guides/cli.md +3 -1
  74. package/docs-md/help/troubleshooting.md +1 -1
  75. package/docs-md/index.json +29 -1
  76. package/docs-md/screen-ui/back-and-gestures.md +3 -1
  77. package/docs-md/screen-ui/bar-buttons.md +1 -1
  78. package/docs-md/screen-ui/toolbar-and-fab.md +2 -0
  79. package/docs-md/start/installation.md +16 -1
  80. package/ios/Animations/ANNFrameDriver.h +55 -15
  81. package/ios/Animations/ANNFrameDriver.mm +214 -60
  82. package/ios/Animations/ANNTransitionAnimator.h +26 -5
  83. package/ios/Animations/ANNTransitionAnimator.mm +49 -3
  84. package/ios/Animations/ANNTransitionPlan.h +2 -2
  85. package/ios/Animations/ANNTransitionPlan.mm +10 -0
  86. package/ios/Animations/Elements/ANNElementFinder.h +2 -2
  87. package/ios/Animations/Elements/ANNElementFinder.mm +2 -2
  88. package/ios/Animations/Elements/ANNElementTransition.h +3 -3
  89. package/ios/Animations/Elements/ANNElementTransition.mm +2 -2
  90. package/ios/Animations/Elements/ANNSharedElementFlight.h +3 -3
  91. package/ios/Animations/Elements/ANNSharedElementFlight.mm +181 -43
  92. package/ios/Animations/Interpolators/ANNViewTracks.h +6 -5
  93. package/ios/Animations/Interpolators/ANNViewTracks.mm +51 -21
  94. package/ios/CLAUDE.md +15 -7
  95. package/ios/CORE.md +1 -1
  96. package/ios/Core/ANNAppDelegate.mm +0 -2
  97. package/ios/Core/ANNDeepLinkQueue.h +6 -5
  98. package/ios/Core/ANNDeepLinkQueue.mm +22 -10
  99. package/ios/Core/ANNSceneDelegate.mm +2 -2
  100. package/ios/Core/ApexNavigation.mm +6 -2
  101. package/ios/Layouts/Root/ANNRootCoordinator.mm +45 -15
  102. package/ios/Layouts/Screen/ANNComponentViewController.mm +4 -1
  103. package/ios/Layouts/Stack/ANNStackAnimatingDelegate.h +11 -2
  104. package/ios/Layouts/Stack/ANNStackAnimatingDelegate.mm +14 -7
  105. package/ios/Layouts/Stack/ANNStackController.h +8 -0
  106. package/ios/Layouts/Stack/ANNStackController.mm +153 -11
  107. package/ios/Layouts/Stack/ANNSwipePopInteraction.h +26 -0
  108. package/ios/Layouts/Stack/ANNSwipePopInteraction.mm +185 -0
  109. package/ios/Layouts/Stack/InteractivePopGestureDelegate.mm +28 -4
  110. package/ios/Options/ANNAnimationOptions.h +2 -1
  111. package/ios/Options/ANNAnimationOptions.mm +1 -1
  112. package/ios/Options/ANNOptions.h +7 -0
  113. package/ios/Options/ANNOptions.mm +45 -0
  114. package/ios/Options/ANNOptionsReader.mm +2 -4
  115. package/ios/Options/ANNStackPopAnimationOptions.h +13 -0
  116. package/ios/Options/ANNStackPopAnimationOptions.mm +11 -0
  117. package/ios/Options/Params/ColorParser.h +4 -3
  118. package/ios/Options/Params/ColorParser.mm +22 -4
  119. package/ios/React/ANNSurfaceHostingView.h +8 -0
  120. package/ios/React/ANNSurfaceHostingView.mm +28 -1
  121. package/ios/React/Slots/ANNSlotScreenView.mm +8 -2
  122. package/ios/TurboModules/ANNTurboModule.mm +46 -0
  123. package/ios/cpp/react/renderer/components/ApexNavigationSpec/ANNSlotState.cpp +20 -0
  124. package/ios/cpp/react/renderer/components/ApexNavigationSpec/ANNSlotState.h +5 -14
  125. package/lib/module/ARCHITECTURE.md +9 -3
  126. package/lib/module/adapters/NativeANNTurboModule.js.map +1 -1
  127. package/lib/module/api/app/AppRegistry.js +14 -2
  128. package/lib/module/api/app/AppRegistry.js.map +1 -1
  129. package/lib/module/api/app/createNavigation.js +188 -66
  130. package/lib/module/api/app/createNavigation.js.map +1 -1
  131. package/lib/module/api/events/navigationEvents.js +6 -2
  132. package/lib/module/api/events/navigationEvents.js.map +1 -1
  133. package/lib/module/api/linking/linking.js +11 -0
  134. package/lib/module/api/linking/linking.js.map +1 -1
  135. package/lib/module/api/screens/defineScreen.js +45 -3
  136. package/lib/module/api/screens/defineScreen.js.map +1 -1
  137. package/lib/module/api/screens/nativeScreens.js +31 -6
  138. package/lib/module/api/screens/nativeScreens.js.map +1 -1
  139. package/lib/module/api/screens/registerScreens.js +172 -35
  140. package/lib/module/api/screens/registerScreens.js.map +1 -1
  141. package/lib/module/api/theme/theme.js +5 -0
  142. package/lib/module/api/theme/theme.js.map +1 -1
  143. package/lib/module/engine/commands.js +21 -1
  144. package/lib/module/engine/commands.js.map +1 -1
  145. package/lib/module/engine/layout.js +7 -2
  146. package/lib/module/engine/layout.js.map +1 -1
  147. package/lib/module/engine/nativeScreens.js +9 -0
  148. package/lib/module/engine/nativeScreens.js.map +1 -1
  149. package/lib/module/engine/options.js +5 -4
  150. package/lib/module/engine/options.js.map +1 -1
  151. package/lib/module/engine/optionsChange.js +102 -0
  152. package/lib/module/engine/optionsChange.js.map +1 -0
  153. package/lib/module/engine/refresh.js +85 -0
  154. package/lib/module/engine/refresh.js.map +1 -0
  155. package/lib/module/engine/rememberedOptions.js +60 -0
  156. package/lib/module/engine/rememberedOptions.js.map +1 -1
  157. package/lib/module/engine/screens.js +26 -0
  158. package/lib/module/engine/screens.js.map +1 -1
  159. package/lib/typescript/adapters/NativeANNTurboModule.d.ts +7 -0
  160. package/lib/typescript/adapters/NativeANNTurboModule.d.ts.map +1 -1
  161. package/lib/typescript/api/app/AppRegistry.d.ts.map +1 -1
  162. package/lib/typescript/api/app/createNavigation.d.ts +5 -0
  163. package/lib/typescript/api/app/createNavigation.d.ts.map +1 -1
  164. package/lib/typescript/api/events/navigationEvents.d.ts +2 -2
  165. package/lib/typescript/api/events/navigationEvents.d.ts.map +1 -1
  166. package/lib/typescript/api/index.d.ts +1 -1
  167. package/lib/typescript/api/index.d.ts.map +1 -1
  168. package/lib/typescript/api/linking/linking.d.ts +8 -0
  169. package/lib/typescript/api/linking/linking.d.ts.map +1 -1
  170. package/lib/typescript/api/screens/defineScreen.d.ts +23 -4
  171. package/lib/typescript/api/screens/defineScreen.d.ts.map +1 -1
  172. package/lib/typescript/api/screens/nativeScreens.d.ts +8 -1
  173. package/lib/typescript/api/screens/nativeScreens.d.ts.map +1 -1
  174. package/lib/typescript/api/screens/registerScreens.d.ts.map +1 -1
  175. package/lib/typescript/api/theme/theme.d.ts +2 -0
  176. package/lib/typescript/api/theme/theme.d.ts.map +1 -1
  177. package/lib/typescript/engine/commands.d.ts +5 -0
  178. package/lib/typescript/engine/commands.d.ts.map +1 -1
  179. package/lib/typescript/engine/layout.d.ts.map +1 -1
  180. package/lib/typescript/engine/nativeScreens.d.ts +5 -0
  181. package/lib/typescript/engine/nativeScreens.d.ts.map +1 -1
  182. package/lib/typescript/engine/optionsChange.d.ts +28 -0
  183. package/lib/typescript/engine/optionsChange.d.ts.map +1 -0
  184. package/lib/typescript/engine/refresh.d.ts +35 -0
  185. package/lib/typescript/engine/refresh.d.ts.map +1 -0
  186. package/lib/typescript/engine/rememberedOptions.d.ts +13 -0
  187. package/lib/typescript/engine/rememberedOptions.d.ts.map +1 -1
  188. package/lib/typescript/engine/screens.d.ts +7 -0
  189. package/lib/typescript/engine/screens.d.ts.map +1 -1
  190. package/lib/typescript/interfaces/ComponentEvents.d.ts +2 -2
  191. package/lib/typescript/interfaces/options/animations.d.ts +40 -15
  192. package/lib/typescript/interfaces/options/animations.d.ts.map +1 -1
  193. package/lib/typescript/interfaces/options/bottomTabs.d.ts +2 -2
  194. package/lib/typescript/interfaces/options/buttons.d.ts +2 -2
  195. package/lib/typescript/interfaces/options/modals.d.ts +6 -7
  196. package/lib/typescript/interfaces/options/modals.d.ts.map +1 -1
  197. package/lib/typescript/interfaces/options/stack.d.ts +8 -3
  198. package/lib/typescript/interfaces/options/stack.d.ts.map +1 -1
  199. package/lib/typescript/interfaces/options/topBar.d.ts +14 -0
  200. package/lib/typescript/interfaces/options/topBar.d.ts.map +1 -1
  201. package/metro/acceptPlugin.js +57 -0
  202. package/metro/babelTransformer.js +45 -0
  203. package/metro/index.js +36 -0
  204. package/package.json +3 -1
  205. package/src/ARCHITECTURE.md +9 -3
  206. package/src/adapters/NativeANNTurboModule.ts +7 -0
  207. package/src/api/app/AppRegistry.ts +16 -2
  208. package/src/api/app/createNavigation.ts +198 -66
  209. package/src/api/events/navigationEvents.ts +9 -3
  210. package/src/api/index.ts +1 -0
  211. package/src/api/linking/linking.ts +22 -1
  212. package/src/api/screens/defineScreen.ts +49 -5
  213. package/src/api/screens/nativeScreens.ts +40 -4
  214. package/src/api/screens/registerScreens.tsx +170 -42
  215. package/src/api/theme/theme.ts +5 -0
  216. package/src/engine/commands.ts +21 -0
  217. package/src/engine/layout.ts +11 -2
  218. package/src/engine/nativeScreens.ts +10 -1
  219. package/src/engine/options.ts +6 -8
  220. package/src/engine/optionsChange.ts +107 -0
  221. package/src/engine/refresh.ts +97 -0
  222. package/src/engine/rememberedOptions.ts +70 -0
  223. package/src/engine/screens.tsx +24 -0
  224. package/src/interfaces/ComponentEvents.ts +2 -2
  225. package/src/interfaces/options/animations.ts +40 -15
  226. package/src/interfaces/options/bottomTabs.ts +2 -2
  227. package/src/interfaces/options/buttons.ts +2 -2
  228. package/src/interfaces/options/modals.ts +6 -7
  229. package/src/interfaces/options/stack.ts +8 -3
  230. package/src/interfaces/options/topBar.ts +14 -0
  231. package/android/src/main/java/dev/apexnative/navigation/views/element/ElementStub.kt +0 -31
  232. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ImageMatrixAnimator.kt +0 -134
  233. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/PlainViewAnimator.kt +0 -78
  234. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/PropertyAnimatorCreator.kt +0 -33
  235. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ReactImageAnimator.kt +0 -57
  236. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ViewChangeAnimator.kt +0 -146
@@ -1,8 +1,14 @@
1
1
  import { AppRegistry as ReactNativeAppRegistry } from 'react-native';
2
2
  import { onNativeEvent } from '../../engine/native';
3
+ import { familyOf } from '../../engine/refresh';
3
4
  import { SLOT_HOST_MODULE } from '../../engine/slots';
4
5
  import { SlotHost } from '../slots/SlotHost';
5
- import { isNavigationApp, type NavigationApp, startApp } from './createNavigation';
6
+ import {
7
+ isNavigationApp,
8
+ type NavigationApp,
9
+ noteReplacingApp,
10
+ startApp,
11
+ } from './createNavigation';
6
12
 
7
13
  type NavigationAppProvider = () => NavigationApp<any, any>;
8
14
 
@@ -35,7 +41,15 @@ function registerComponent(
35
41
  navigation: NavigationApp<any, any> | NavigationAppProvider
36
42
  ): string {
37
43
  if (__DEV__ && registered) {
38
- console.warn(`[navigation] '${key}' replaces the app registered as '${appKey}'`);
44
+ // The entry file running again after an edit (Fast Refresh) registers its new version of the same app under the
45
+ // same name; Metro puts one it exports in the family of the old one once the file ran.
46
+ if (key === appKey) noteReplacingApp(navigation);
47
+ const before = { key: appKey, app: registered };
48
+ Promise.resolve().then(() => {
49
+ const family = familyOf(before.app);
50
+ if (key === before.key || (family && family === familyOf(navigation))) return;
51
+ console.warn(`[navigation] '${key}' replaces the app registered as '${before.key}'`);
52
+ });
39
53
  }
40
54
  appKey = key;
41
55
  registered = navigation;
@@ -1,7 +1,10 @@
1
1
  import type { ComponentType } from 'react';
2
+ import { DevSettings } from 'react-native';
2
3
  import { setDefaultOptions, setRoot } from '../../engine/commands';
3
4
  import { type AppProvider, enableSlots, withSlots } from '../../engine/slots';
4
5
  import { listenToNativeScreens, startHost } from '../../engine/nativeScreens';
6
+ import { sameValue } from '../../engine/optionsChange';
7
+ import { familyOf, keepStateOf, refreshRuns } from '../../engine/refresh';
5
8
  import { listenToSplitViews } from '../../engine/places';
6
9
  import { hasNativeScreenHooks } from '../../engine/screens';
7
10
  import type { LayoutRoot } from '../../interfaces/Layout';
@@ -24,7 +27,10 @@ import type { ErrorFallbackProps } from '../screens/ScreenBoundary';
24
27
  import { getAppContext, setAppContext } from '../context/appContext';
25
28
  import {
26
29
  type AppOptionsInput,
30
+ getDirection,
31
+ getTheme,
27
32
  initTheme,
33
+ setTheme,
28
34
  type NavigationDirection,
29
35
  type NavigationTheme,
30
36
  themedDefaultOptions,
@@ -51,6 +57,7 @@ import {
51
57
  linksStarted,
52
58
  reportLink,
53
59
  setupLinking,
60
+ updateLinking,
54
61
  } from '../linking/linking';
55
62
  import type { LinkMatch } from '../linking/linkPaths';
56
63
  import type { LinkingConfig } from '../linking/types';
@@ -86,6 +93,9 @@ const starts = new WeakMap<NavigationApp<any, any>, () => Promise<void>>();
86
93
 
87
94
  /** Starts the app, or brings back its root when the native side launches again. */
88
95
  export function startApp(app: NavigationApp<any, any>): Promise<void> {
96
+ // An app made after another launched knows once its module ran whether it is that app's next version.
97
+ const waiting = pending.get(app);
98
+ if (waiting) return waiting.then(() => startApp(app));
89
99
  const start = starts.get(app);
90
100
  if (!start) throw new Error('[navigation] Not an app made by createNavigation');
91
101
  return start();
@@ -240,7 +250,11 @@ type CheckedRoots<Root, Roots, AppScreens> = {
240
250
  };
241
251
 
242
252
  export function isNavigationApp(value: unknown): value is NavigationApp {
243
- return typeof value === 'object' && value !== null && starts.has(value as NavigationApp);
253
+ return (
254
+ typeof value === 'object' &&
255
+ value !== null &&
256
+ (starts.has(value as NavigationApp) || pending.has(value as NavigationApp))
257
+ );
244
258
  }
245
259
 
246
260
  /**
@@ -277,14 +291,18 @@ export function createNavigation<
277
291
  > &
278
292
  NoInfer<CheckedRoots<FirstLayout, Roots, AllScreens<AppScreens, Groups>>>
279
293
  ): NavigationApp<Roots, AllScreens<AppScreens, Groups>> {
294
+ // The app that launched before, when this call may be its module running again after an edit (Fast Refresh): what
295
+ // the call starts waits until that is known.
296
+ const launched = __DEV__ ? hotCandidate() : undefined;
280
297
  // Counted from here: a "back again to leave" listener of the app's entry file comes after this call.
281
- watchBackListeners();
298
+ if (!launched) watchBackListeners();
282
299
  // idle: not started yet, or the last start failed. mounting: the first root is on its way. mounted: it is shown.
283
300
  let phase: 'idle' | 'mounting' | 'mounted' = 'idle';
284
301
  let screensRegistered = false;
285
302
  // The splash shows at the first start of this runtime only: a start that failed and runs again goes without it.
286
303
  let splashChecked = false;
287
304
  let listening = false;
305
+ let removeListeners: (() => void) | undefined;
288
306
  let prepared: Promise<void> | undefined;
289
307
  let markMounted!: () => void;
290
308
  // A new activity came while the first root was on its way (Android): its link is taken once that root is shown.
@@ -325,6 +343,62 @@ export function createNavigation<
325
343
  return buildRoot(typeof root === 'function' ? root(context) : root);
326
344
  }
327
345
 
346
+ // The screens of the app, its groups, the screen of a failed start and the splash, with what wraps each of them.
347
+ function registerAppScreens() {
348
+ const wrapper = {
349
+ errorFallback: config.errorFallback,
350
+ loadingFallback: config.loadingFallback,
351
+ onError:
352
+ config.onError &&
353
+ ((error: unknown, screen: ScreenInfo, componentStack: string | null | undefined) =>
354
+ config.onError?.(error, { type: 'screen', screen, componentStack })),
355
+ };
356
+ registerScreens(
357
+ [
358
+ { source: 'screens', screens: config.screens ?? {} },
359
+ ...Object.entries(config.groups ?? {}).map(([group, { screens, options }]) => ({
360
+ source: `the group '${group}'`,
361
+ screens,
362
+ options,
363
+ })),
364
+ ...(config.errorFallback
365
+ ? [
366
+ {
367
+ source: 'the start',
368
+ screens: { [START_FAILURE_SCREEN]: startFailureScreen(config.errorFallback) },
369
+ },
370
+ ]
371
+ : []),
372
+ ],
373
+ { ...wrapper, provider: config.screenProvider }
374
+ );
375
+ // The splash renders before willMount ends: a root of its own, without the app's providers, which may need what
376
+ // willMount prepares.
377
+ if (config.splash) {
378
+ registerScreens(
379
+ [
380
+ {
381
+ source: 'the splash',
382
+ screens: { [SPLASH_SCREEN]: config.splash.component },
383
+ ownRoot: true,
384
+ },
385
+ ],
386
+ {
387
+ ...wrapper,
388
+ // A splash that failed to render can't call hide(): the start goes on without waiting for it.
389
+ onError: (
390
+ error: unknown,
391
+ screen: ScreenInfo,
392
+ componentStack: string | null | undefined
393
+ ) => {
394
+ releaseSplash();
395
+ wrapper.onError?.(error, screen, componentStack);
396
+ },
397
+ }
398
+ );
399
+ }
400
+ }
401
+
328
402
  async function mount() {
329
403
  phase = 'mounting';
330
404
  // The state before the first root: the links wait for a newer one, which tells the screen they act from.
@@ -333,54 +407,7 @@ export function createNavigation<
333
407
  let linkHandled = false;
334
408
  try {
335
409
  if (!screensRegistered) {
336
- const wrapper = {
337
- errorFallback: config.errorFallback,
338
- loadingFallback: config.loadingFallback,
339
- onError:
340
- config.onError &&
341
- ((error: unknown, screen: ScreenInfo, componentStack: string | null | undefined) =>
342
- config.onError?.(error, { type: 'screen', screen, componentStack })),
343
- };
344
- registerScreens(
345
- [
346
- { source: 'screens', screens: config.screens ?? {} },
347
- ...Object.entries(config.groups ?? {}).map(([group, { screens, options }]) => ({
348
- source: `the group '${group}'`,
349
- screens,
350
- options,
351
- })),
352
- ...(config.errorFallback
353
- ? [
354
- {
355
- source: 'the start',
356
- screens: { [START_FAILURE_SCREEN]: startFailureScreen(config.errorFallback) },
357
- },
358
- ]
359
- : []),
360
- ],
361
- { ...wrapper, provider: config.screenProvider }
362
- );
363
- // The splash renders before willMount ends: a root of its own, without the app's providers, which may need what
364
- // willMount prepares.
365
- if (config.splash) {
366
- registerScreens(
367
- [
368
- {
369
- source: 'the splash',
370
- screens: { [SPLASH_SCREEN]: config.splash.component },
371
- ownRoot: true,
372
- },
373
- ],
374
- {
375
- ...wrapper,
376
- // A splash that failed to render can't call hide(): the start goes on without waiting for it.
377
- onError: (error: unknown, screen: ScreenInfo, componentStack: string | null | undefined) => {
378
- releaseSplash();
379
- wrapper.onError?.(error, screen, componentStack);
380
- },
381
- }
382
- );
383
- }
410
+ registerAppScreens();
384
411
  screensRegistered = true;
385
412
  }
386
413
  // The link that launched the app is asked for first: on iOS the first React content to appear (the host below,
@@ -485,20 +512,13 @@ export function createNavigation<
485
512
  }
486
513
 
487
514
  async function launch() {
515
+ if (__DEV__) launchedApp = app;
488
516
  setActiveApp({ roots: config.roots, presentation: config.presentation, mounted });
489
517
  if (!listening) {
490
518
  listening = true;
491
519
  // The edges of `safeArea` go under the app's options; the window's color goes with them, as the native side
492
520
  // takes it from the default options.
493
- initTheme(
494
- config.theme,
495
- {
496
- under: config.safeArea && { layout: { safeAreaEdges: config.safeArea.edges } },
497
- options: config.options,
498
- over: config.window && { window: config.window },
499
- },
500
- config.direction
501
- );
521
+ initTheme(config.theme, appDefaults(), config.direction);
502
522
  if (config.context !== undefined) setAppContext(config.context);
503
523
  if (config.appProvider) enableSlots(config.appProvider);
504
524
  listenToNavigationState();
@@ -507,7 +527,7 @@ export function createNavigation<
507
527
  listenToNativeScreens();
508
528
  listenToSplitViews();
509
529
  listenToNativeScreenCalls();
510
- if (config.listeners) addNavigationListeners(config.listeners);
530
+ if (config.listeners) removeListeners = addNavigationListeners(config.listeners);
511
531
  }
512
532
  launchState.launch++;
513
533
  // A new activity has no default options: the last ones set come back, else the theme's and the app's.
@@ -528,19 +548,131 @@ export function createNavigation<
528
548
  relaunchWaiters.splice(0).forEach((resume) => resume());
529
549
  }
530
550
 
531
- // The links listen from now on: one that comes before the native side launches waits for the first root.
532
- if (config.linking) {
533
- setupLinking(config.linking, {
551
+ // The routes of the links, the roots they name and where their errors go.
552
+ function linkingOptions() {
553
+ return {
534
554
  roots: config.roots,
535
- onError: config.onError && ((error, url) => config.onError?.(error, { type: 'link', url })),
536
- });
555
+ onError:
556
+ config.onError &&
557
+ ((error: unknown, url: string) => config.onError?.(error, { type: 'link', url })),
558
+ };
559
+ }
560
+
561
+ // Fast Refresh ran the module that makes the app again (development): its new config takes the place of the old.
562
+ // What only the start reads (the first root, willMount, the splash, the context) has done its part; the screens, the
563
+ // default options, the roots, the listeners and the links take the new one. A theme the same as before leaves the one
564
+ // the app set since. The providers and fallbacks are in the families of the ones before (`keepStateOf`): React
565
+ // Refresh puts the new ones in place, with the state of what they wrap.
566
+ function update(next: typeof config) {
567
+ const before = config;
568
+ // The app's one tree is set up at the start, or not: a provider added or taken out starts the app again.
569
+ if (!before.appProvider !== !next.appProvider) {
570
+ DevSettings.reload('[navigation] appProvider was added or taken out');
571
+ return;
572
+ }
573
+ config = next;
574
+ if (screensRegistered) registerAppScreens();
575
+ setActiveApp({ roots: config.roots, presentation: config.presentation, mounted });
576
+ // A direction the app set since stays too, unless the edit changes the config's.
577
+ const defaultsBefore = themedDefaultOptions();
578
+ const direction = sameValue(before.direction, next.direction) ? getDirection() : next.direction;
579
+ initTheme(getTheme(), appDefaults(), direction);
580
+ if (!sameValue(before.theme, next.theme)) {
581
+ setTheme(next.theme ?? {});
582
+ } else {
583
+ const defaults = themedDefaultOptions();
584
+ if (!sameValue(defaultsBefore, defaults)) setDefaultOptions(defaults ?? {});
585
+ }
586
+ if (listening) {
587
+ removeListeners?.();
588
+ removeListeners = config.listeners && addNavigationListeners(config.listeners);
589
+ }
590
+ if (config.linking && linkingStarted) updateLinking(config.linking, linkingOptions());
591
+ else startLinking();
592
+ }
593
+
594
+ // The default options of the app, under and over the theme's.
595
+ function appDefaults() {
596
+ return {
597
+ under: config.safeArea && { layout: { safeAreaEdges: config.safeArea.edges } },
598
+ options: config.options,
599
+ over: config.window && { window: config.window },
600
+ };
601
+ }
602
+
603
+ // The links listen from now on: one that comes before the native side launches waits for the first root.
604
+ let linkingStarted = false;
605
+ function startLinking() {
606
+ if (!config.linking || linkingStarted) return;
607
+ linkingStarted = true;
608
+ setupLinking(config.linking, linkingOptions());
609
+ }
610
+
611
+ // The components of the app keep their state across Fast Refresh, as the screens' do.
612
+ function keepComponentsState() {
613
+ if (!__DEV__) return;
614
+ keepStateOf(app, config.appProvider, 'appProvider');
615
+ keepStateOf(app, config.screenProvider, 'screenProvider');
616
+ keepStateOf(app, config.errorFallback, 'errorFallback');
617
+ keepStateOf(app, config.loadingFallback, 'loadingFallback');
618
+ keepStateOf(app, config.splash?.component, 'splash');
537
619
  }
538
620
 
539
621
  const app: NavigationApp<Roots, AllScreens<AppScreens, Groups>> = { [NAVIGATION_APP]: true };
540
- starts.set(app, () => reportStart(launch));
622
+ keepComponentsState();
623
+ const start = () => {
624
+ startLinking();
625
+ starts.set(app, () => reportStart(launch));
626
+ if (__DEV__) updates.set(app, update as (config: unknown) => void);
627
+ };
628
+ if (!launched) {
629
+ start();
630
+ return app;
631
+ }
632
+ // Once the module ran, Metro has put its exports in the families of the ones of its run before: the same family as
633
+ // the app that launched, or the app's entry registering it under the app's name again (an app it doesn't export),
634
+ // means this is its module again, and the app that runs takes the new config.
635
+ pending.set(
636
+ app,
637
+ Promise.resolve().then(() => {
638
+ pending.delete(app);
639
+ const family = familyOf(app);
640
+ if ((family && family === familyOf(launched)) || replacing.delete(app)) {
641
+ updates.get(launched)?.(config);
642
+ starts.set(app, starts.get(launched)!);
643
+ updates.set(app, updates.get(launched)!);
644
+ return;
645
+ }
646
+ watchBackListeners();
647
+ start();
648
+ })
649
+ );
541
650
  return app;
542
651
  }
543
652
 
653
+ // The app that launched in this runtime, and what takes the config of its module when it runs again (development).
654
+ let launchedApp: NavigationApp<any, any> | undefined;
655
+ const updates = new WeakMap<NavigationApp<any, any>, (config: unknown) => void>();
656
+ // The apps made after the launch until their module ran, and those the app's entry registered again under its name.
657
+ const pending = new WeakMap<NavigationApp<any, any>, Promise<void>>();
658
+ const replacing = new WeakSet<NavigationApp<any, any>>();
659
+
660
+ /**
661
+ * Notes an app the entry file registers under the name of the app that launched: the entry ran again after an edit
662
+ * (development), and the new app is the old one's next version.
663
+ */
664
+ export function noteReplacingApp(app: unknown): void {
665
+ if (__DEV__ && launchedApp && typeof app === 'object' && app !== null && NAVIGATION_APP in app) {
666
+ replacing.add(app as NavigationApp<any, any>);
667
+ }
668
+ }
669
+
670
+ // The app that launched, when React Refresh runs and a new app may be its module running again: one its module
671
+ // exports (Metro gives it a family), or one the entry registers again under the app's name.
672
+ function hotCandidate(): NavigationApp<any, any> | undefined {
673
+ return launchedApp && refreshRuns() ? launchedApp : undefined;
674
+ }
675
+
544
676
  // A willMount that never ends keeps the splash screen for good: in development the app hears of it.
545
677
  function warnIfLong(willMount: Promise<void>) {
546
678
  const timer = setTimeout(
@@ -166,10 +166,16 @@ export function onNavigationEvent<Name extends NavigationEventName>(
166
166
  return onNativeEvent(nativeName, deliver);
167
167
  }
168
168
 
169
- /** Listens to the events of `listeners`, for as long as the app runs. */
170
- export function addNavigationListeners(listeners: NavigationListeners) {
169
+ /** Listens to the events of `listeners` until the function it gives is called. */
170
+ export function addNavigationListeners(listeners: NavigationListeners): () => void {
171
+ const removers: (() => void)[] = [];
171
172
  for (const name of Object.keys(listeners) as NavigationEventName[]) {
172
173
  const handler = listeners[name];
173
- if (handler) onNavigationEvent(name, handler as (event: NavigationEvents[typeof name]) => void);
174
+ if (handler) {
175
+ removers.push(
176
+ onNavigationEvent(name, handler as (event: NavigationEvents[typeof name]) => void)
177
+ );
178
+ }
174
179
  }
180
+ return () => removers.forEach((remove) => remove());
175
181
  }
package/src/api/index.ts CHANGED
@@ -109,6 +109,7 @@ export type { NativeScreen } from './screens/nativeScreens';
109
109
  export { defineArrangement } from './screens/arrangement';
110
110
  export type { Arrangement, ArrangementDefinition, ScreenArrangement } from './screens/arrangement';
111
111
  export type {
112
+ DefinedScreen,
112
113
  OptionsEnvironment,
113
114
  ScreenDefinition,
114
115
  ScreenOptionsInput,
@@ -6,7 +6,11 @@ import { commandsStarted, whenCommandsSettle } from '../commands/errors';
6
6
  import { getAppContext, subscribeAppContext } from '../context/appContext';
7
7
  import { buildRoot, type RootLayout } from '../layout/Root';
8
8
  import { navigation } from '../navigation';
9
- import { getNavigationState, whenScreenAfter, type NavigationState } from '../state/navigationState';
9
+ import {
10
+ getNavigationState,
11
+ whenScreenAfter,
12
+ type NavigationState,
13
+ } from '../state/navigationState';
10
14
  import {
11
15
  compileRoutes,
12
16
  matchLink,
@@ -68,6 +72,23 @@ export function setupLinking(
68
72
  setPendingLinkOpener((link) => run(link, routeOf(link), false));
69
73
  }
70
74
 
75
+ /**
76
+ * Takes the routes, the roots and `onError` of an app whose module ran again after an edit (Fast Refresh, development):
77
+ * the links that come from now on find the new routes.
78
+ */
79
+ export function updateLinking(
80
+ linking: Config,
81
+ options: {
82
+ roots: RootsMap | undefined;
83
+ onError: ((error: unknown, url: string) => void) | undefined;
84
+ }
85
+ ): void {
86
+ config = linking;
87
+ roots = options.roots;
88
+ onError = options.onError;
89
+ routes = compileRoutes(Object.keys(linking.routes));
90
+ }
91
+
71
92
  /**
72
93
  * The link that launched the app, matched as the app starts (`unmatched` may run commands then); the same one for
73
94
  * every try of the first start.
@@ -1,5 +1,6 @@
1
- import type { ComponentType } from 'react';
1
+ import { type ComponentType, createElement } from 'react';
2
2
  import type { Options } from '../../interfaces/Options';
3
+ import { keepStateOf, newestType, redefined } from '../../engine/refresh';
3
4
  import { getAppContext } from '../context/appContext';
4
5
  import { readScreenOptions, ScreenOptions } from '../options/ScreenOptions';
5
6
  import { getTheme, type NavigationTheme } from '../theme/theme';
@@ -38,12 +39,55 @@ export interface ScreenDefinition<Props = any> {
38
39
  arrangement?: ScreenArrangement<Props & object>;
39
40
  }
40
41
 
42
+ const SCREEN_DEFINITION = Symbol('apexnative.navigation.screenDefinition');
43
+
44
+ /**
45
+ * A screen of `defineScreen`: a component of its own, which renders the screen's component and carries the screen's
46
+ * definition. It goes right into `screens` of createNavigation, and rendered on its own (a test) it renders the screen.
47
+ */
48
+ export type DefinedScreen<Props> = ComponentType<Props> & {
49
+ readonly [SCREEN_DEFINITION]: ScreenDefinition<Props>;
50
+ };
51
+
52
+ /**
53
+ * Describes a screen next to its component, so its registration stays one line. It gives a component that renders the
54
+ * screen and goes right into `screens` of createNavigation: a file that exports only its screens stays a boundary of
55
+ * Fast Refresh, so an edit of the screen, its options too, updates it in place instead of reloading the app.
56
+ */
57
+ export function defineScreen<Props>(definition: ScreenDefinition<Props>): DefinedScreen<Props> {
58
+ const { component } = definition;
59
+ // A component of each definition, so the app's own component stays as it is for any other use of it.
60
+ const Component = component as ComponentType<object>;
61
+ function Screen(props: Props) {
62
+ return createElement(Component, props as object);
63
+ }
64
+ Screen.displayName = component.displayName ?? component.name;
65
+ Object.defineProperty(Screen, SCREEN_DEFINITION, { value: definition });
66
+ keepStateOf(Screen, component);
67
+ redefined(Screen);
68
+ return Screen as DefinedScreen<Props>;
69
+ }
70
+
41
71
  /**
42
- * Describes a screen next to its component, so its registration stays one line. The same object can go right into
43
- * `screens` of createNavigation.
72
+ * The definition a screen of `defineScreen` carries, the newest one after Fast Refresh ran the screen's module again;
73
+ * undefined for anything else.
44
74
  */
45
- export function defineScreen<Props>(definition: ScreenDefinition<Props>): ScreenDefinition<Props> {
46
- return definition;
75
+ export function definitionOf(value: unknown): ScreenDefinition | undefined {
76
+ return isDefinedScreen(value) ? ownDefinitionOf(newestType(value)) : undefined;
77
+ }
78
+
79
+ /** The definition a screen of `defineScreen` carries itself, not a newer one; undefined for anything else. */
80
+ export function ownDefinitionOf(value: unknown): ScreenDefinition | undefined {
81
+ return isDefinedScreen(value) ? value[SCREEN_DEFINITION] : undefined;
82
+ }
83
+
84
+ /** Whether the value is a screen of `defineScreen`. */
85
+ export function isDefinedScreen(value: unknown): value is DefinedScreen<any> {
86
+ return (
87
+ (typeof value === 'function' || typeof value === 'object') &&
88
+ value !== null &&
89
+ SCREEN_DEFINITION in value
90
+ );
47
91
  }
48
92
 
49
93
  export function buildScreenOptions<Props>(
@@ -1,3 +1,5 @@
1
+ import { useEffect } from 'react';
2
+ import { redefined, resetOnEdit } from '../../engine/refresh';
1
3
  import type { ScreenArrangement } from './arrangement';
2
4
  import type { ScreenOptionsInput } from './defineScreen';
3
5
 
@@ -30,7 +32,9 @@ export interface NativeScreenDefinition<Props extends object> {
30
32
 
31
33
  /**
32
34
  * A native screen for `screens`: commands and layouts name it like any other screen, and its props go to the native
33
- * creator. `nativeName` is the name it is registered under natively (`ApexNavigation.registerNativeScreen`).
35
+ * creator. `nativeName` is the name it is registered under natively (`ApexNavigation.registerNativeScreen`). It is a
36
+ * component that runs `useProps`, so a file that exports only its screens updates by Fast Refresh, the hook and the
37
+ * options too.
34
38
  *
35
39
  * @example
36
40
  * const Settings = nativeScreen('Settings', {
@@ -49,10 +53,42 @@ export function nativeScreen<Props extends object = {}>(
49
53
  nativeName: string,
50
54
  definition: NativeScreenDefinition<Props> = {}
51
55
  ): NativeScreen<Props> {
52
- const { options, useProps, arrangement } = definition;
53
- return { [NATIVE_SCREEN]: true, nativeName, options, useProps, arrangement };
56
+ const { options, useProps = noProps, arrangement } = definition;
57
+ // A component that runs the hook, so that a file exporting only its screens stays a boundary of Fast Refresh: React
58
+ // Refresh puts the new version in place, with the edited hook.
59
+ function NativeScreen({ props, onProps }: NativeScreenHookProps<Props>) {
60
+ const result = useProps(props);
61
+ // After each render React committed, so the native side never shows what the app did not.
62
+ useEffect(() => onProps(result));
63
+ return null;
64
+ }
65
+ const screen = Object.assign(NativeScreen, {
66
+ [NATIVE_SCREEN]: true as const,
67
+ nativeName,
68
+ options,
69
+ useProps: definition.useProps,
70
+ arrangement,
71
+ });
72
+ // The hook is the app's and may have other hooks after an edit: its new version mounts anew.
73
+ resetOnEdit(screen);
74
+ redefined(screen);
75
+ return screen;
76
+ }
77
+
78
+ function noProps(): object {
79
+ return {};
80
+ }
81
+
82
+ /** What the component of a native screen renders with: the props of the command, and where the hook's result goes. */
83
+ export interface NativeScreenHookProps<Props> {
84
+ props: Props;
85
+ onProps(result: object): void;
54
86
  }
55
87
 
56
88
  export function isNativeScreen(value: unknown): value is NativeScreen<any> {
57
- return typeof value === 'object' && value !== null && NATIVE_SCREEN in value;
89
+ return (
90
+ (typeof value === 'function' || typeof value === 'object') &&
91
+ value !== null &&
92
+ NATIVE_SCREEN in value
93
+ );
58
94
  }