@granite-js/react-native 0.0.1

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 (189) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/LICENSE +202 -0
  3. package/README.md +24 -0
  4. package/bin/cli.js +3 -0
  5. package/cli.d.ts +1 -0
  6. package/cli.js +4 -0
  7. package/config.d.ts +2 -0
  8. package/config.js +5 -0
  9. package/dist/app/App/index.android.d.ts +2 -0
  10. package/dist/app/App/index.ios.d.ts +6 -0
  11. package/dist/app/Granite.d.ts +60 -0
  12. package/dist/app/index.d.ts +2 -0
  13. package/dist/app/registerPage.d.ts +20 -0
  14. package/dist/async-bridges.d.ts +2 -0
  15. package/dist/constant-bridges.d.ts +1 -0
  16. package/dist/dev-entrypoint/index.d.ts +2 -0
  17. package/dist/event/abstract.d.ts +42 -0
  18. package/dist/event/index.d.ts +2 -0
  19. package/dist/event/useGraniteEvent.d.ts +14 -0
  20. package/dist/impression-area/ImpressionArea.d.ts +231 -0
  21. package/dist/impression-area/index.d.ts +1 -0
  22. package/dist/index.d.ts +17 -0
  23. package/dist/initial-props/InitialProps.d.ts +127 -0
  24. package/dist/initial-props/index.d.ts +1 -0
  25. package/dist/intersection-observer/IOContext.d.ts +10 -0
  26. package/dist/intersection-observer/IOFlatList.d.ts +55 -0
  27. package/dist/intersection-observer/IOManager.d.ts +24 -0
  28. package/dist/intersection-observer/IOScrollView.d.ts +59 -0
  29. package/dist/intersection-observer/InView.d.ts +107 -0
  30. package/dist/intersection-observer/IntersectionObserver.d.ts +67 -0
  31. package/dist/intersection-observer/index.d.ts +8 -0
  32. package/dist/intersection-observer/withIO.d.ts +20 -0
  33. package/dist/jest/index.d.ts +1 -0
  34. package/dist/jest/index.js +32 -0
  35. package/dist/keyboard/KeyboardAboveView.d.ts +40 -0
  36. package/dist/keyboard/index.d.ts +1 -0
  37. package/dist/keyboard/useKeyboardAnimatedHeight.d.ts +20 -0
  38. package/dist/native-event-emitter/eventEmitters/index.d.ts +2 -0
  39. package/dist/native-event-emitter/eventEmitters/types.d.ts +4 -0
  40. package/dist/native-event-emitter/eventEmitters/visibilityChanged.d.ts +10 -0
  41. package/dist/native-event-emitter/index.d.ts +1 -0
  42. package/dist/native-event-emitter/nativeEventEmitter.d.ts +15 -0
  43. package/dist/native-modules/core/GraniteCoreModule.d.ts +8 -0
  44. package/dist/native-modules/index.d.ts +3 -0
  45. package/dist/native-modules/natives/GraniteModule.d.ts +7 -0
  46. package/dist/native-modules/natives/closeView.d.ts +21 -0
  47. package/dist/native-modules/natives/getSchemeUri.d.ts +23 -0
  48. package/dist/native-modules/natives/index.d.ts +3 -0
  49. package/dist/native-modules/natives/openURL.d.ts +36 -0
  50. package/dist/react/index.d.ts +1 -0
  51. package/dist/react/useWaitForReturnNavigator.d.ts +39 -0
  52. package/dist/rn-polyfills/index.d.ts +1 -0
  53. package/dist/rn-polyfills/symbol-asynciterator/index.d.ts +9 -0
  54. package/dist/rn-polyfills/url/index.d.ts +1 -0
  55. package/dist/router/Router.d.ts +59 -0
  56. package/dist/router/components/BackButton.d.ts +7 -0
  57. package/dist/router/components/CanGoBackGuard.d.ts +6 -0
  58. package/dist/router/components/RouterBackButton.d.ts +9 -0
  59. package/dist/router/components/StackNavigator.d.ts +54 -0
  60. package/dist/router/constants.d.ts +2 -0
  61. package/dist/router/createRoute.d.ts +39 -0
  62. package/dist/router/createRoute.test-d.d.ts +9 -0
  63. package/dist/router/hooks/useInitialRouteName.d.ts +1 -0
  64. package/dist/router/hooks/useIsInitialScreen.d.ts +1 -0
  65. package/dist/router/hooks/useRouterControls.d.ts +11 -0
  66. package/dist/router/index.d.ts +3 -0
  67. package/dist/router/types/RequireContext.d.ts +7 -0
  68. package/dist/router/types/RouteScreen.d.ts +16 -0
  69. package/dist/router/types/Screen.d.ts +23 -0
  70. package/dist/router/types/index.d.ts +3 -0
  71. package/dist/router/types/screen-option.d.ts +4 -0
  72. package/dist/router/utils/createParentRouteScreenMap.d.ts +8 -0
  73. package/dist/router/utils/defaultParserParams.d.ts +9 -0
  74. package/dist/router/utils/index.d.ts +2 -0
  75. package/dist/router/utils/matchers.d.ts +2 -0
  76. package/dist/router/utils/mergeParentLayoutScreen.d.ts +18 -0
  77. package/dist/router/utils/path.d.ts +53 -0
  78. package/dist/router/utils/screen.d.ts +53 -0
  79. package/dist/scroll-view-inertial-background/ScrollViewInertialBackground.d.ts +49 -0
  80. package/dist/scroll-view-inertial-background/index.d.ts +1 -0
  81. package/dist/types/global.d.ts +18 -0
  82. package/dist/use-back-event/index.d.ts +1 -0
  83. package/dist/use-back-event/useBackEvent.d.ts +135 -0
  84. package/dist/utils/noop.d.ts +1 -0
  85. package/dist/utils/usePreservedCallback.d.ts +1 -0
  86. package/dist/visibility/VisibilityProvider.d.ts +27 -0
  87. package/dist/visibility/index.d.ts +6 -0
  88. package/dist/visibility/react-navigation/index.d.ts +2 -0
  89. package/dist/visibility/react-navigation/useIsFocusedSafely.d.ts +20 -0
  90. package/dist/visibility/react-navigation/useNavigationSafely.d.ts +19 -0
  91. package/dist/visibility/useIsAppForeground.d.ts +39 -0
  92. package/dist/visibility/useVisibility.d.ts +35 -0
  93. package/dist/visibility/useVisibilityChange.d.ts +51 -0
  94. package/dist/visibility/useVisibilityChanged.d.ts +41 -0
  95. package/dist/visibility/utils/usePrevious.d.ts +15 -0
  96. package/jest.d.ts +1 -0
  97. package/package.json +92 -0
  98. package/presets.d.ts +1 -0
  99. package/src/app/App/index.android.tsx +6 -0
  100. package/src/app/App/index.d.ts +6 -0
  101. package/src/app/App/index.ios.tsx +13 -0
  102. package/src/app/Granite.tsx +130 -0
  103. package/src/app/index.ts +2 -0
  104. package/src/app/registerPage.ts +29 -0
  105. package/src/async-bridges.ts +2 -0
  106. package/src/constant-bridges.ts +1 -0
  107. package/src/dev-entrypoint/index.tsx +21 -0
  108. package/src/event/abstract.ts +130 -0
  109. package/src/event/index.ts +2 -0
  110. package/src/event/useGraniteEvent.ts +34 -0
  111. package/src/impression-area/ImpressionArea.tsx +341 -0
  112. package/src/impression-area/index.ts +1 -0
  113. package/src/index.ts +19 -0
  114. package/src/initial-props/InitialProps.ts +144 -0
  115. package/src/initial-props/index.ts +1 -0
  116. package/src/intersection-observer/IOContext.ts +16 -0
  117. package/src/intersection-observer/IOFlatList.ts +72 -0
  118. package/src/intersection-observer/IOManager.ts +73 -0
  119. package/src/intersection-observer/IOScrollView.ts +69 -0
  120. package/src/intersection-observer/InView.tsx +205 -0
  121. package/src/intersection-observer/IntersectionObserver.ts +212 -0
  122. package/src/intersection-observer/index.ts +24 -0
  123. package/src/intersection-observer/withIO.tsx +151 -0
  124. package/src/jest/index.ts +1 -0
  125. package/src/keyboard/KeyboardAboveView.tsx +62 -0
  126. package/src/keyboard/index.ts +1 -0
  127. package/src/keyboard/useKeyboardAnimatedHeight.tsx +81 -0
  128. package/src/native-event-emitter/eventEmitters/index.ts +3 -0
  129. package/src/native-event-emitter/eventEmitters/types.ts +4 -0
  130. package/src/native-event-emitter/eventEmitters/visibilityChanged.ts +11 -0
  131. package/src/native-event-emitter/index.ts +1 -0
  132. package/src/native-event-emitter/nativeEventEmitter.ts +18 -0
  133. package/src/native-modules/core/GraniteCoreModule.ts +9 -0
  134. package/src/native-modules/index.ts +3 -0
  135. package/src/native-modules/natives/GraniteModule.ts +8 -0
  136. package/src/native-modules/natives/closeView.ts +25 -0
  137. package/src/native-modules/natives/getSchemeUri.ts +27 -0
  138. package/src/native-modules/natives/index.ts +3 -0
  139. package/src/native-modules/natives/openURL.ts +40 -0
  140. package/src/react/index.ts +1 -0
  141. package/src/react/useWaitForReturnNavigator.ts +75 -0
  142. package/src/rn-polyfills/index.ts +7 -0
  143. package/src/rn-polyfills/symbol-asynciterator/index.ts +15 -0
  144. package/src/rn-polyfills/url/index.ts +1 -0
  145. package/src/router/Router.tsx +164 -0
  146. package/src/router/components/BackButton.tsx +58 -0
  147. package/src/router/components/CanGoBackGuard.tsx +31 -0
  148. package/src/router/components/RouterBackButton.tsx +32 -0
  149. package/src/router/components/StackNavigator.tsx +12 -0
  150. package/src/router/constants.ts +3 -0
  151. package/src/router/createRoute.test-d.ts +52 -0
  152. package/src/router/createRoute.ts +161 -0
  153. package/src/router/hooks/useInitialRouteName.tsx +22 -0
  154. package/src/router/hooks/useIsInitialScreen.ts +7 -0
  155. package/src/router/hooks/useRouterControls.tsx +72 -0
  156. package/src/router/index.ts +3 -0
  157. package/src/router/types/RequireContext.ts +7 -0
  158. package/src/router/types/RouteScreen.ts +17 -0
  159. package/src/router/types/Screen.tsx +24 -0
  160. package/src/router/types/index.ts +3 -0
  161. package/src/router/types/screen-option.ts +23 -0
  162. package/src/router/utils/createParentRouteScreenMap.spec.ts +166 -0
  163. package/src/router/utils/createParentRouteScreenMap.ts +136 -0
  164. package/src/router/utils/defaultParserParams.spec.ts +46 -0
  165. package/src/router/utils/defaultParserParams.ts +19 -0
  166. package/src/router/utils/index.ts +2 -0
  167. package/src/router/utils/matchers.ts +5 -0
  168. package/src/router/utils/mergeParentLayoutScreen.spec.tsx +112 -0
  169. package/src/router/utils/mergeParentLayoutScreen.tsx +43 -0
  170. package/src/router/utils/path.spec.ts +135 -0
  171. package/src/router/utils/path.ts +105 -0
  172. package/src/router/utils/screen.tsx +111 -0
  173. package/src/scroll-view-inertial-background/ScrollViewInertialBackground.tsx +99 -0
  174. package/src/scroll-view-inertial-background/index.ts +1 -0
  175. package/src/types/global.ts +31 -0
  176. package/src/use-back-event/index.ts +1 -0
  177. package/src/use-back-event/useBackEvent.tsx +260 -0
  178. package/src/utils/noop.ts +1 -0
  179. package/src/utils/usePreservedCallback.ts +16 -0
  180. package/src/visibility/VisibilityProvider.tsx +36 -0
  181. package/src/visibility/index.ts +6 -0
  182. package/src/visibility/react-navigation/index.ts +2 -0
  183. package/src/visibility/react-navigation/useIsFocusedSafely.tsx +58 -0
  184. package/src/visibility/react-navigation/useNavigationSafely.tsx +30 -0
  185. package/src/visibility/useIsAppForeground.tsx +73 -0
  186. package/src/visibility/useVisibility.tsx +54 -0
  187. package/src/visibility/useVisibilityChange.ts +69 -0
  188. package/src/visibility/useVisibilityChanged.tsx +69 -0
  189. package/src/visibility/utils/usePrevious.tsx +24 -0
@@ -0,0 +1,31 @@
1
+ import { ReactNode, useEffect } from 'react';
2
+ import { BackHandler } from 'react-native';
3
+
4
+ export function CanGoBackGuard({
5
+ children,
6
+ canGoBack,
7
+ onBack,
8
+ }: {
9
+ canGoBack: boolean;
10
+ children: ReactNode;
11
+ onBack?: () => void;
12
+ }) {
13
+ const shouldBlockGoingBack = !canGoBack;
14
+
15
+ useEffect(() => {
16
+ if (shouldBlockGoingBack) {
17
+ const subscription = BackHandler.addEventListener('hardwareBackPress', () => {
18
+ onBack?.();
19
+ return true;
20
+ });
21
+
22
+ return () => {
23
+ subscription.remove();
24
+ };
25
+ }
26
+
27
+ return;
28
+ }, [shouldBlockGoingBack, onBack]);
29
+
30
+ return <>{children}</>;
31
+ }
@@ -0,0 +1,32 @@
1
+ import { NavigationContainerRefWithCurrent } from '@granite-js/native/@react-navigation/native';
2
+ import { BackButton } from './BackButton';
3
+ import { closeView } from '../../native-modules';
4
+
5
+ export type RouterBackButtonProps = {
6
+ onPress?: () => void;
7
+ tintColor?: string;
8
+ canGoBack?: boolean;
9
+ onBack?: () => void;
10
+ navigationContainerRef: NavigationContainerRefWithCurrent<any>;
11
+ };
12
+
13
+ export function RouterBackButton({ tintColor, canGoBack, onBack, navigationContainerRef }: RouterBackButtonProps) {
14
+ return (
15
+ <BackButton
16
+ tintColor={tintColor}
17
+ onPress={() => {
18
+ onBack?.();
19
+
20
+ if (!canGoBack) {
21
+ return;
22
+ }
23
+
24
+ if (navigationContainerRef.canGoBack()) {
25
+ navigationContainerRef.goBack();
26
+ } else {
27
+ closeView();
28
+ }
29
+ }}
30
+ />
31
+ );
32
+ }
@@ -0,0 +1,12 @@
1
+ import { createNativeStackNavigator } from '@granite-js/native/@react-navigation/native-stack';
2
+
3
+ function createStackNavigator() {
4
+ const Stack = createNativeStackNavigator();
5
+
6
+ return {
7
+ Navigator: Stack.Navigator,
8
+ Screen: Stack.Screen,
9
+ };
10
+ }
11
+
12
+ export const StackNavigator = createStackNavigator();
@@ -0,0 +1,3 @@
1
+ export const RESERVED_KEYWORDS = ['_layout'];
2
+
3
+ export const RESERVED_PATHS = ['/_404'];
@@ -0,0 +1,52 @@
1
+ import { assertType, describe, it } from 'vitest';
2
+ import { createRoute, useParams } from './createRoute';
3
+
4
+ declare module './createRoute' {
5
+ interface RegisterScreen {
6
+ '/test': {
7
+ id: string;
8
+ name: string;
9
+ };
10
+ }
11
+ }
12
+
13
+ describe('createRoute', () => {
14
+ const Route = createRoute('/test', {
15
+ component: () => null,
16
+ validateParams: (params) => params as { id: string; name: string },
17
+ });
18
+
19
+ it('useParams', () => {
20
+ assertType<{
21
+ id: string;
22
+ name: string;
23
+ }>(Route.useParams());
24
+
25
+ assertType<Readonly<object | undefined>>(useParams({ strict: false }));
26
+
27
+ // @ts-expect-error - Type error should occur when strict is false
28
+ assertType<{ id: string; name: string }>(useParams({ strict: false }));
29
+
30
+ assertType<{
31
+ id: string;
32
+ name: string;
33
+ }>(useParams({ from: '/test', strict: true }));
34
+
35
+ assertType<{
36
+ id: string;
37
+ name: string;
38
+ }>(useParams({ from: '/test' }));
39
+
40
+ // @ts-expect-error Type error should occur when no options
41
+ assertType(useParams());
42
+
43
+ // @ts-expect-error Type error should occur when empty object
44
+ assertType(useParams({}));
45
+
46
+ // @ts-expect-error Type error should occur when path is not registered
47
+ assertType(useParams({ from: '/abcdefg' }));
48
+
49
+ // @ts-expect-error Type error should occur since 'from' and 'strict: false' are conflicting options
50
+ assertType(useParams({ from: '/test', strict: false }));
51
+ });
52
+ });
@@ -0,0 +1,161 @@
1
+ import {
2
+ type ParamListBase,
3
+ useNavigation as useNavigationNative,
4
+ useRoute,
5
+ } from '@granite-js/native/@react-navigation/native';
6
+ import { NativeStackNavigationProp } from '@granite-js/native/@react-navigation/native-stack';
7
+ import { useMemo } from 'react';
8
+ import { RESERVED_PATHS } from './constants';
9
+ import { defaultParserParams } from './utils/defaultParserParams';
10
+
11
+ export interface RouteOptions<T extends Readonly<object | undefined>> {
12
+ parserParams?: (params: Record<string, unknown>) => Record<string, unknown>;
13
+ validateParams?: (params: Readonly<object | undefined>) => T;
14
+ component: React.FC<any>;
15
+ }
16
+
17
+ export type NavigationProps = NativeStackNavigationProp<
18
+ // @ts-expect-error - override type
19
+ keyof RegisterScreen extends never ? ParamListBase : RegisterScreen
20
+ >;
21
+
22
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
23
+ export interface RegisterScreen {}
24
+
25
+ export function useNavigation() {
26
+ return useNavigationNative<NavigationProps>();
27
+ }
28
+
29
+ export type RouteHooksOptions<TScreen extends keyof RegisterScreen> =
30
+ | {
31
+ from: TScreen;
32
+ strict?: true;
33
+ }
34
+ | {
35
+ strict: false;
36
+ from?: never;
37
+ };
38
+
39
+ export const routeMap = new Map<
40
+ keyof RegisterScreen,
41
+ { options: Omit<RouteOptions<any>, 'component'>; component: React.FC<any> }
42
+ >();
43
+
44
+ export function useMatchOptions<TScreen extends keyof RegisterScreen>(options: RouteHooksOptions<TScreen>) {
45
+ const route = useRoute();
46
+ const from = 'from' in options ? options.from : (route.name as TScreen);
47
+ const strict = 'from' in options ? true : options.strict;
48
+
49
+ if (strict && from !== route.name) {
50
+ throw new Error(`Cannot access parameters from route '${from}' in current route '${route.name}'`);
51
+ }
52
+
53
+ return useMemo(() => {
54
+ if (!from) {
55
+ return null;
56
+ }
57
+
58
+ if (!(routeMap.has(from) || RESERVED_PATHS.includes(from))) {
59
+ throw new Error(`Route '${from}' is not registered`);
60
+ }
61
+
62
+ const routeOptions = routeMap.get(from);
63
+ return {
64
+ ...routeOptions?.options,
65
+ parserParams: routeOptions?.options.parserParams ?? defaultParserParams,
66
+ };
67
+ }, [from]);
68
+ }
69
+
70
+ export function useParams<TScreen extends keyof RegisterScreen>(options: {
71
+ from: TScreen;
72
+ strict?: true;
73
+ }): RegisterScreen[TScreen];
74
+ export function useParams(options: { strict: false }): Readonly<object | undefined>;
75
+
76
+ /**
77
+ * @public
78
+ * @category Screen Control
79
+ * @name useParams
80
+ * @description
81
+ *
82
+ * `useParams` is a hook that retrieves parameters from a specified route.
83
+ * Using this hook, you can easily access parameters of the current route.
84
+ * With the `validateParams` option, you can validate parameter structure and transform types,
85
+ * reducing runtime errors and writing safer code.
86
+ *
87
+ * @param {RouteHooksOptions<TScreen>} options Object containing information about the route to retrieve.
88
+ * @param {string} [options.from] Route path to retrieve parameters from. If not specified, retrieves parameters from the current route. Must be specified when strict mode is true.
89
+ * @param {boolean} [options.strict] Strict mode setting. When set to true, throws an error if the specified route doesn't match the current route. When set to false, skips validateParams validation and returns parameters of the current screen as is.
90
+ *
91
+ * @example
92
+ * ### Retrieving Route Parameters
93
+ *
94
+ *
95
+ * ::: code-group
96
+ *
97
+ * ```tsx [pages/examples/use-params.tsx]
98
+ * import React from 'react';
99
+ * import { Text } from 'react-native';
100
+ * import { createRoute, useParams } from '@granite-js/react-native';
101
+ *
102
+ * export const Route = createRoute('/examples/use-params', {
103
+ * validateParams: (params) => params as { id: string },
104
+ * component: UseParamsExample,
105
+ * });
106
+ *
107
+ * function UseParamsExample() {
108
+ * // First method: Using the useParams method of the route object
109
+ * const params = Route.useParams();
110
+ *
111
+ * // Second method: Using the useParams hook directly
112
+ * const params2 = useParams({ from: '/examples/use-params' });
113
+ *
114
+ * // Third method: Using with strict mode set to false
115
+ * // When strict is false, retrieves parameters from the current route
116
+ * // and skips validation even if validateParams is defined
117
+ * const params3 = useParams({ strict: false }) as { id: string };
118
+ *
119
+ * return (
120
+ * <>
121
+ * <Text>{params.id}</Text>
122
+ * <Text>{params2.id}</Text>
123
+ * <Text>{params3.id}</Text>
124
+ * </>
125
+ * );
126
+ * }
127
+ * ```
128
+ * :::
129
+ */
130
+ export function useParams<TScreen extends keyof RegisterScreen>(
131
+ options: RouteHooksOptions<TScreen>
132
+ ): TScreen extends keyof RegisterScreen ? RegisterScreen[TScreen] : Readonly<object | undefined> {
133
+ const routeOptions = useMatchOptions(options);
134
+ const route = useRoute();
135
+
136
+ const params = useMemo(() => {
137
+ if (!routeOptions) {
138
+ return (route.params ?? {}) as Readonly<object | undefined>;
139
+ }
140
+
141
+ const parsedParams = routeOptions.parserParams(route.params as Record<string, string>);
142
+ return options.strict && routeOptions.validateParams ? routeOptions.validateParams(parsedParams) : parsedParams;
143
+ }, [routeOptions, route.params, options.strict]);
144
+
145
+ return params;
146
+ }
147
+
148
+ export const createRoute = <T extends Readonly<object | undefined>>(
149
+ path: keyof RegisterScreen,
150
+ options: RouteOptions<T>
151
+ ) => {
152
+ const { component, ...restOptions } = options;
153
+ routeMap.set(path, { options: restOptions, component });
154
+
155
+ const _path = path as keyof RegisterScreen;
156
+ return {
157
+ _path,
158
+ useNavigation,
159
+ useParams: () => useParams({ from: _path, strict: true }) as T,
160
+ };
161
+ };
@@ -0,0 +1,22 @@
1
+ import { Platform } from 'react-native';
2
+ import { getSchemeUri } from '../../native-modules';
3
+
4
+ export function useInitialRouteName(prefix: string) {
5
+ const initialScheme = getInitialScheme();
6
+ const pathname = initialScheme?.slice(prefix.length).split('?')[0];
7
+ const shouldUseIndex = initialScheme == null || pathname?.length === 0;
8
+
9
+ return shouldUseIndex ? '/' : pathname;
10
+ }
11
+ function getInitialScheme() {
12
+ const scheme = getSchemeUri();
13
+
14
+ /**
15
+ * Removes trailing '/' on Android.
16
+ */
17
+ if (Platform.OS === 'android') {
18
+ return scheme?.replaceAll(/\/+$/g, '');
19
+ }
20
+
21
+ return scheme;
22
+ }
@@ -0,0 +1,7 @@
1
+ import { useNavigationState } from '@granite-js/native/@react-navigation/native';
2
+
3
+ export function useIsInitialScreen() {
4
+ const index = useNavigationState((state) => state?.index ?? 0);
5
+
6
+ return index === 0;
7
+ }
@@ -0,0 +1,72 @@
1
+ import { NavigationContainer } from '@granite-js/native/@react-navigation/native';
2
+ import { useMemo, type ComponentProps, type ComponentType, type PropsWithChildren } from 'react';
3
+ import { getSchemeUri } from '../../native-modules';
4
+ import { StackNavigator } from '../components/StackNavigator';
5
+ import { RESERVED_KEYWORDS } from '../constants';
6
+ import { RequireContext } from '../types/RequireContext';
7
+ import { getRouteScreens, getScreenPathMapConfig } from '../utils';
8
+ import { createParentRouteScreenMap } from '../utils/createParentRouteScreenMap';
9
+ import { mergeParentLayoutScreen } from '../utils/mergeParentLayoutScreen';
10
+ import { getFileNameFromPath } from '../utils/path';
11
+
12
+ type NavigationContainerProps = ComponentProps<typeof NavigationContainer>;
13
+
14
+ export interface RouterControlsConfig {
15
+ prefix: string;
16
+ context: RequireContext;
17
+ screenContainer?: ComponentType<PropsWithChildren<any>>;
18
+ }
19
+
20
+ export function useRouterControls({ prefix, context, screenContainer: ScreenContainer }: RouterControlsConfig) {
21
+ const routeScreens = useMemo(() => getRouteScreens(context), [context]);
22
+
23
+ const registerScreens = useMemo(() => {
24
+ return routeScreens.filter(
25
+ (screen) => !RESERVED_KEYWORDS.includes(getFileNameFromPath(screen.path, { withExtension: false }))
26
+ );
27
+ }, [routeScreens]);
28
+
29
+ const layoutScreenMap = useMemo(() => createParentRouteScreenMap(routeScreens, '_layout'), [routeScreens]);
30
+
31
+ const Screens = useMemo(() => {
32
+ return registerScreens.map((routeScreen) => {
33
+ const Layout = mergeParentLayoutScreen(layoutScreenMap, routeScreen.path);
34
+
35
+ const Component = function Component() {
36
+ const element = (
37
+ <Layout>
38
+ <routeScreen.component />
39
+ </Layout>
40
+ );
41
+
42
+ return ScreenContainer == null ? element : <ScreenContainer>{element}</ScreenContainer>;
43
+ };
44
+
45
+ const routePath = routeScreen.path;
46
+ const screenOptions = routeScreen.component?.screenOptions ?? {};
47
+
48
+ return <StackNavigator.Screen key={routePath} name={routePath} component={Component} options={screenOptions} />;
49
+ });
50
+ }, [registerScreens, layoutScreenMap, ScreenContainer]);
51
+
52
+ const linkingOptions: NavigationContainerProps['linking'] = useMemo(() => {
53
+ return {
54
+ prefixes: [prefix],
55
+ config: {
56
+ screens: getScreenPathMapConfig(registerScreens),
57
+ },
58
+ async getInitialURL() {
59
+ const initialURL = getSchemeUri();
60
+
61
+ if (initialURL == null) {
62
+ return;
63
+ }
64
+
65
+ /** @NOTE Korean paths need to be decoded. */
66
+ return decodeURI(initialURL);
67
+ },
68
+ };
69
+ }, [prefix, registerScreens]);
70
+
71
+ return { Screens, linkingOptions };
72
+ }
@@ -0,0 +1,3 @@
1
+ export * from './Router';
2
+ export { getRouteScreens, getScreenPathMapConfig, defaultParserParams } from './utils';
3
+ export * from './types';
@@ -0,0 +1,7 @@
1
+ export interface RequireContext {
2
+ keys(): string[];
3
+ (id: string): any;
4
+ <T>(id: string): T;
5
+ resolve(id: string): string;
6
+ id: string;
7
+ }
@@ -0,0 +1,17 @@
1
+ import { Screen } from './Screen';
2
+
3
+ /**
4
+ * @name RouteScreen
5
+ */
6
+ export interface RouteScreen {
7
+ /**
8
+ * @name path
9
+ * @description Path information (e.g. "/", "/list", "/list/:id", etc.)
10
+ */
11
+ path: string;
12
+ /**
13
+ * @name component
14
+ * @description Screen component
15
+ */
16
+ component: Screen;
17
+ }
@@ -0,0 +1,24 @@
1
+ import type { NativeStackNavigationOptions } from '@granite-js/native/@react-navigation/native-stack';
2
+ import type { ComponentType } from 'react';
3
+
4
+ /**
5
+ * @name Screen
6
+ * @description A Screen that serves as a unit of navigation
7
+ * @example
8
+ *
9
+ * ```ts
10
+ * function Page() {
11
+ * // ...
12
+ * }
13
+ *
14
+ * Page.screenOptions = {
15
+ * // ...
16
+ * }
17
+ * ```
18
+ */
19
+ export type Screen = ComponentType & GraniteScreenOptions;
20
+
21
+ interface GraniteScreenOptions {
22
+ /** @description Add this when customization is needed for NativeStack Screen's screenOptions */
23
+ screenOptions?: NativeStackNavigationOptions;
24
+ }
@@ -0,0 +1,3 @@
1
+ export * from './RequireContext';
2
+ export * from './RouteScreen';
3
+ export * from './Screen';
@@ -0,0 +1,23 @@
1
+ import { NativeStackNavigationOptions } from '@granite-js/native/@react-navigation/native-stack';
2
+ import { Platform } from 'react-native';
3
+
4
+ export const DEFAULT_BACKGROUND_COLOR = '#ffffff';
5
+ export const DEFAULT_HEADER_TINT_COLOR = '#333d4b';
6
+ export const BASE_STACK_NAVIGATOR_STYLE: NativeStackNavigationOptions = {
7
+ contentStyle: {
8
+ backgroundColor: DEFAULT_BACKGROUND_COLOR,
9
+ },
10
+ headerTintColor: DEFAULT_HEADER_TINT_COLOR,
11
+ headerTitleStyle: {
12
+ color: 'transparent',
13
+ },
14
+ headerStyle: {
15
+ backgroundColor: DEFAULT_BACKGROUND_COLOR,
16
+ },
17
+ headerShadowVisible: false,
18
+
19
+ // FIX
20
+ // According to the docs, react-navigation's Android transition behavior should follow the OS default, but in reality, it doesn't.
21
+ // We've decided to solve this issue by unifying the animation to slide up from bottom.
22
+ animation: Platform.OS === 'android' ? 'fade_from_bottom' : 'default',
23
+ };
@@ -0,0 +1,166 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { createParentRouteScreenMap } from './createParentRouteScreenMap';
3
+ import type { RouteScreen } from '../types/RouteScreen';
4
+
5
+ describe('createParentRouteScreenMap', () => {
6
+ describe('When there are nested layouts', () => {
7
+ it('Each page is mapped to its nearest parent layout', () => {
8
+ const paths: RouteScreen[] = [
9
+ { path: './granite-module/_layout', component: () => null },
10
+ { path: './granite-module/test', component: () => null },
11
+ { path: './about/_layout', component: () => null },
12
+ { path: './about/test', component: () => null },
13
+ { path: './about', component: () => null },
14
+ { path: './image', component: () => null },
15
+ ];
16
+
17
+ const layoutMap = createParentRouteScreenMap(paths, '_layout');
18
+
19
+ // Pages in granite-module directory are mapped to their layout
20
+ expect(layoutMap.get('./granite-module/test')?.path).toBe('./granite-module/_layout');
21
+
22
+ // Pages in about directory are mapped to their layout
23
+ expect(layoutMap.get('./about')?.path).toBe('./about/_layout');
24
+ expect(layoutMap.get('./about/test')?.path).toBe('./about/_layout');
25
+
26
+ // Pages without layout are undefined
27
+ expect(layoutMap.get('./image')).toBeUndefined();
28
+
29
+ // Layout files are not mapped because there's no layout at the root
30
+ expect(layoutMap.get('./granite-module/_layout')).toBeUndefined();
31
+ expect(layoutMap.get('./about/_layout')).toBeUndefined();
32
+ });
33
+
34
+ it('If there is a root layout, pages without layout are mapped to the root layout', () => {
35
+ const paths: RouteScreen[] = [
36
+ { path: './_layout', component: () => null },
37
+ { path: './image', component: () => null },
38
+ { path: './granite-module/_layout', component: () => null },
39
+ { path: './granite-module/test', component: () => null },
40
+ { path: './about/_layout', component: () => null },
41
+ { path: './about', component: () => null },
42
+ { path: './about/test', component: () => null },
43
+ ];
44
+
45
+ const layoutMap = createParentRouteScreenMap(paths, '_layout');
46
+
47
+ // Pages in each directory are mapped to their layout
48
+ expect(layoutMap.get('./granite-module/test')?.path).toBe('./granite-module/_layout');
49
+ expect(layoutMap.get('./about')?.path).toBe('./about/_layout');
50
+ expect(layoutMap.get('./about/test')?.path).toBe('./about/_layout');
51
+
52
+ // Pages without layout are mapped to root layout
53
+ expect(layoutMap.get('./image')?.path).toBe('./_layout');
54
+ expect(layoutMap.get('./_layout')).toBeUndefined();
55
+ expect(layoutMap.get('./granite-module/_layout')?.path).toBe('./_layout');
56
+ expect(layoutMap.get('./about/_layout')?.path).toBe('./_layout');
57
+ });
58
+
59
+ it('Layout mapping works correctly in nested directory structure', () => {
60
+ const paths: RouteScreen[] = [
61
+ { path: './nested', component: () => null },
62
+ { path: './nested/_layout', component: () => null },
63
+ { path: './nested/test', component: () => null },
64
+ ];
65
+
66
+ const layoutMap = createParentRouteScreenMap(paths, '_layout');
67
+
68
+ // nested is mapped to nested/_layout
69
+ expect(layoutMap.get('./nested')?.path).toBe('./nested/_layout');
70
+
71
+ // nested/test is mapped to nested/_layout
72
+ expect(layoutMap.get('./nested/test')?.path).toBe('./nested/_layout');
73
+
74
+ expect(layoutMap.get('./nested/_layout')?.path).toBeUndefined();
75
+ });
76
+
77
+ it('Layout mapping works correctly in double nested directory structure', () => {
78
+ const paths: RouteScreen[] = [
79
+ { path: './nested/_layout', component: () => null },
80
+ { path: './nested/test', component: () => null },
81
+ { path: './nested/deep/_layout', component: () => null },
82
+ { path: './nested/deep/test', component: () => null },
83
+ ];
84
+
85
+ const layoutMap = createParentRouteScreenMap(paths, '_layout');
86
+
87
+ // nested/test is mapped to nested/_layout
88
+ expect(layoutMap.get('./nested/test')?.path).toBe('./nested/_layout');
89
+
90
+ // nested/deep/test is mapped to nested/deep/_layout
91
+ expect(layoutMap.get('./nested/deep/test')?.path).toBe('./nested/deep/_layout');
92
+
93
+ // nested/deep/_layout is mapped to nested/_layout
94
+ expect(layoutMap.get('./nested/deep/_layout')?.path).toBe('./nested/_layout');
95
+
96
+ // nested/_layout is not mapped
97
+ expect(layoutMap.get('./nested/_layout')?.path).toBeUndefined();
98
+ });
99
+
100
+ it('Layout mapping works correctly in triple nested directory structure', () => {
101
+ const paths: RouteScreen[] = [
102
+ { path: './nested/_layout', component: () => null },
103
+ { path: './nested/test', component: () => null },
104
+ { path: './nested/deep/_layout', component: () => null },
105
+ { path: './nested/deep/test', component: () => null },
106
+ { path: './nested/deep/deeper/_layout', component: () => null },
107
+ { path: './nested/deep/deeper/test', component: () => null },
108
+ ];
109
+
110
+ const layoutMap = createParentRouteScreenMap(paths, '_layout');
111
+
112
+ // nested/test is mapped to nested/_layout
113
+ expect(layoutMap.get('./nested/test')?.path).toBe('./nested/_layout');
114
+
115
+ // nested/deep/test is mapped to nested/deep/_layout
116
+ expect(layoutMap.get('./nested/deep/test')?.path).toBe('./nested/deep/_layout');
117
+
118
+ // nested/deep/deeper/test is mapped to nested/deep/deeper/_layout
119
+ expect(layoutMap.get('./nested/deep/deeper/test')?.path).toBe('./nested/deep/deeper/_layout');
120
+
121
+ // nested/deep/deeper/_layout is mapped to nested/deep/_layout
122
+ expect(layoutMap.get('./nested/deep/deeper/_layout')?.path).toBe('./nested/deep/_layout');
123
+
124
+ // nested/deep/_layout is mapped to nested/_layout
125
+ expect(layoutMap.get('./nested/deep/_layout')?.path).toBe('./nested/_layout');
126
+
127
+ // nested/_layout is not mapped
128
+ expect(layoutMap.get('./nested/_layout')?.path).toBeUndefined();
129
+ });
130
+ });
131
+
132
+ describe('When there are no layouts', () => {
133
+ it('All pages are mapped to undefined', () => {
134
+ const paths: RouteScreen[] = [
135
+ { path: './page1', component: () => null },
136
+ { path: './page2', component: () => null },
137
+ { path: './nested/page3', component: () => null },
138
+ { path: './nested/deep/page4', component: () => null },
139
+ ];
140
+
141
+ const layoutMap = createParentRouteScreenMap(paths, '_layout');
142
+
143
+ // All pages are undefined
144
+ expect(layoutMap.get('./page1')).toBeUndefined();
145
+ expect(layoutMap.get('./page2')).toBeUndefined();
146
+ expect(layoutMap.get('./nested/page3')).toBeUndefined();
147
+ expect(layoutMap.get('./nested/deep/page4')).toBeUndefined();
148
+ });
149
+ });
150
+
151
+ it('Nested _layout is mapped to parent _layout', () => {
152
+ const paths: RouteScreen[] = [
153
+ { path: './_layout', component: () => null },
154
+ { path: './nested/_layout', component: () => null },
155
+ { path: './nested/test', component: () => null },
156
+ { path: './nested/deep/_layout', component: () => null },
157
+ { path: './nested/deep/test', component: () => null },
158
+ ];
159
+
160
+ const layoutMap = createParentRouteScreenMap(paths, '_layout');
161
+ expect(layoutMap.get('./nested/deep/test')?.path).toBe('./nested/deep/_layout');
162
+ expect(layoutMap.get('./nested/deep/_layout')?.path).toBe('./nested/_layout');
163
+ expect(layoutMap.get('./nested/_layout')?.path).toBe('./_layout');
164
+ expect(layoutMap.get('./_layout')?.path).toBeUndefined();
165
+ });
166
+ });