@granite-js/react-native 0.0.0-dev-20250725013859

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 (217) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +24 -0
  3. package/bin/cli.js +3 -0
  4. package/cli.d.ts +1 -0
  5. package/cli.js +4 -0
  6. package/config.d.ts +2 -0
  7. package/config.js +5 -0
  8. package/dist/app/App/index.android.d.ts +2 -0
  9. package/dist/app/App/index.ios.d.ts +6 -0
  10. package/dist/app/AppRoot.d.ts +1 -0
  11. package/dist/app/Granite.d.ts +61 -0
  12. package/dist/app/HostAppRoot.d.ts +1 -0
  13. package/dist/app/index.d.ts +2 -0
  14. package/dist/async-bridges.d.ts +2 -0
  15. package/dist/blur/BlurView.d.ts +78 -0
  16. package/dist/blur/ReactNativeBlurModule.d.ts +6 -0
  17. package/dist/blur/constants.d.ts +1 -0
  18. package/dist/blur/index.d.ts +1 -0
  19. package/dist/constant-bridges.d.ts +1 -0
  20. package/dist/constants.d.ts +1 -0
  21. package/dist/dev-entrypoint/index.d.ts +2 -0
  22. package/dist/event/abstract.d.ts +42 -0
  23. package/dist/event/index.d.ts +2 -0
  24. package/dist/event/useGraniteEvent.d.ts +14 -0
  25. package/dist/impression-area/ImpressionArea.d.ts +231 -0
  26. package/dist/impression-area/index.d.ts +1 -0
  27. package/dist/index.d.ts +21 -0
  28. package/dist/initial-props/InitialProps.d.ts +92 -0
  29. package/dist/initial-props/index.d.ts +1 -0
  30. package/dist/intersection-observer/IOContext.d.ts +10 -0
  31. package/dist/intersection-observer/IOFlatList.d.ts +55 -0
  32. package/dist/intersection-observer/IOManager.d.ts +24 -0
  33. package/dist/intersection-observer/IOScrollView.d.ts +59 -0
  34. package/dist/intersection-observer/InView.d.ts +107 -0
  35. package/dist/intersection-observer/IntersectionObserver.d.ts +67 -0
  36. package/dist/intersection-observer/index.d.ts +8 -0
  37. package/dist/intersection-observer/withIO.d.ts +20 -0
  38. package/dist/jest/index.d.ts +1 -0
  39. package/dist/jest/index.js +32 -0
  40. package/dist/keyboard/KeyboardAboveView.d.ts +40 -0
  41. package/dist/keyboard/index.d.ts +2 -0
  42. package/dist/keyboard/useKeyboardAnimatedHeight.d.ts +20 -0
  43. package/dist/native-event-emitter/eventEmitters/index.d.ts +2 -0
  44. package/dist/native-event-emitter/eventEmitters/types.d.ts +4 -0
  45. package/dist/native-event-emitter/eventEmitters/visibilityChanged.d.ts +10 -0
  46. package/dist/native-event-emitter/index.d.ts +1 -0
  47. package/dist/native-event-emitter/nativeEventEmitter.d.ts +15 -0
  48. package/dist/native-modules/core/GraniteCoreModule.d.ts +8 -0
  49. package/dist/native-modules/index.d.ts +3 -0
  50. package/dist/native-modules/natives/GraniteModule.d.ts +7 -0
  51. package/dist/native-modules/natives/closeView.d.ts +21 -0
  52. package/dist/native-modules/natives/getSchemeUri.d.ts +23 -0
  53. package/dist/native-modules/natives/index.d.ts +3 -0
  54. package/dist/native-modules/natives/openURL.d.ts +36 -0
  55. package/dist/react/index.d.ts +1 -0
  56. package/dist/react/useWaitForReturnNavigator.d.ts +39 -0
  57. package/dist/rn-polyfills/index.d.ts +1 -0
  58. package/dist/rn-polyfills/symbol-asynciterator/index.d.ts +9 -0
  59. package/dist/rn-polyfills/url/index.d.ts +1 -0
  60. package/dist/router/Router.d.ts +59 -0
  61. package/dist/router/components/BackButton.d.ts +7 -0
  62. package/dist/router/components/CanGoBackGuard.d.ts +6 -0
  63. package/dist/router/components/RouterBackButton.d.ts +9 -0
  64. package/dist/router/components/StackNavigator.d.ts +54 -0
  65. package/dist/router/constants.d.ts +2 -0
  66. package/dist/router/createRoute.d.ts +39 -0
  67. package/dist/router/createRoute.test-d.d.ts +9 -0
  68. package/dist/router/hooks/useInitialRouteName.d.ts +1 -0
  69. package/dist/router/hooks/useIsInitialScreen.d.ts +1 -0
  70. package/dist/router/hooks/useRouterControls.d.ts +11 -0
  71. package/dist/router/index.d.ts +3 -0
  72. package/dist/router/types/RequireContext.d.ts +7 -0
  73. package/dist/router/types/RouteScreen.d.ts +16 -0
  74. package/dist/router/types/Screen.d.ts +23 -0
  75. package/dist/router/types/index.d.ts +3 -0
  76. package/dist/router/types/screen-option.d.ts +4 -0
  77. package/dist/router/utils/createParentRouteScreenMap.d.ts +8 -0
  78. package/dist/router/utils/defaultParserParams.d.ts +9 -0
  79. package/dist/router/utils/index.d.ts +2 -0
  80. package/dist/router/utils/matchers.d.ts +2 -0
  81. package/dist/router/utils/mergeParentLayoutScreen.d.ts +18 -0
  82. package/dist/router/utils/path.d.ts +53 -0
  83. package/dist/router/utils/screen.d.ts +37 -0
  84. package/dist/scroll-view-inertial-background/ScrollViewInertialBackground.d.ts +49 -0
  85. package/dist/scroll-view-inertial-background/index.d.ts +1 -0
  86. package/dist/status-bar/StatusBar.android.d.ts +3 -0
  87. package/dist/status-bar/StatusBar.ios.d.ts +3 -0
  88. package/dist/status-bar/index.d.ts +2 -0
  89. package/dist/status-bar/types.d.ts +20 -0
  90. package/dist/status-bar/utils.d.ts +3 -0
  91. package/dist/types/global.d.ts +14 -0
  92. package/dist/use-back-event/index.d.ts +1 -0
  93. package/dist/use-back-event/useBackEvent.d.ts +135 -0
  94. package/dist/utils/noop.d.ts +1 -0
  95. package/dist/utils/usePreservedCallback.d.ts +1 -0
  96. package/dist/video/Video.d.ts +67 -0
  97. package/dist/video/index.d.ts +1 -0
  98. package/dist/video/instance.d.ts +9 -0
  99. package/dist/visibility/VisibilityProvider.d.ts +27 -0
  100. package/dist/visibility/index.d.ts +6 -0
  101. package/dist/visibility/react-navigation/index.d.ts +2 -0
  102. package/dist/visibility/react-navigation/useIsFocusedSafely.d.ts +20 -0
  103. package/dist/visibility/react-navigation/useNavigationSafely.d.ts +19 -0
  104. package/dist/visibility/useIsAppForeground.d.ts +39 -0
  105. package/dist/visibility/useVisibility.d.ts +35 -0
  106. package/dist/visibility/useVisibilityChange.d.ts +51 -0
  107. package/dist/visibility/useVisibilityChanged.d.ts +41 -0
  108. package/dist/visibility/utils/usePrevious.d.ts +15 -0
  109. package/jest.d.ts +1 -0
  110. package/package.json +94 -0
  111. package/presets.d.ts +1 -0
  112. package/src/app/App/index.android.tsx +6 -0
  113. package/src/app/App/index.d.ts +6 -0
  114. package/src/app/App/index.ios.tsx +13 -0
  115. package/src/app/AppRoot.tsx +39 -0
  116. package/src/app/Granite.tsx +128 -0
  117. package/src/app/HostAppRoot.tsx +19 -0
  118. package/src/app/index.ts +2 -0
  119. package/src/async-bridges.ts +2 -0
  120. package/src/blur/BlurView.tsx +103 -0
  121. package/src/blur/ReactNativeBlurModule.ts +19 -0
  122. package/src/blur/constants.ts +3 -0
  123. package/src/blur/index.ts +1 -0
  124. package/src/constant-bridges.ts +1 -0
  125. package/src/constants.ts +1 -0
  126. package/src/dev-entrypoint/index.tsx +17 -0
  127. package/src/event/abstract.ts +130 -0
  128. package/src/event/index.ts +2 -0
  129. package/src/event/useGraniteEvent.ts +34 -0
  130. package/src/impression-area/ImpressionArea.tsx +341 -0
  131. package/src/impression-area/index.ts +1 -0
  132. package/src/index.ts +24 -0
  133. package/src/initial-props/InitialProps.ts +95 -0
  134. package/src/initial-props/index.ts +1 -0
  135. package/src/intersection-observer/IOContext.ts +16 -0
  136. package/src/intersection-observer/IOFlatList.ts +72 -0
  137. package/src/intersection-observer/IOManager.ts +73 -0
  138. package/src/intersection-observer/IOScrollView.ts +69 -0
  139. package/src/intersection-observer/InView.tsx +205 -0
  140. package/src/intersection-observer/IntersectionObserver.ts +212 -0
  141. package/src/intersection-observer/index.ts +24 -0
  142. package/src/intersection-observer/withIO.tsx +151 -0
  143. package/src/jest/index.ts +1 -0
  144. package/src/keyboard/KeyboardAboveView.tsx +62 -0
  145. package/src/keyboard/index.ts +2 -0
  146. package/src/keyboard/useKeyboardAnimatedHeight.tsx +81 -0
  147. package/src/native-event-emitter/eventEmitters/index.ts +3 -0
  148. package/src/native-event-emitter/eventEmitters/types.ts +4 -0
  149. package/src/native-event-emitter/eventEmitters/visibilityChanged.ts +11 -0
  150. package/src/native-event-emitter/index.ts +1 -0
  151. package/src/native-event-emitter/nativeEventEmitter.ts +18 -0
  152. package/src/native-modules/core/GraniteCoreModule.ts +9 -0
  153. package/src/native-modules/index.ts +3 -0
  154. package/src/native-modules/natives/GraniteModule.ts +8 -0
  155. package/src/native-modules/natives/closeView.ts +25 -0
  156. package/src/native-modules/natives/getSchemeUri.ts +27 -0
  157. package/src/native-modules/natives/index.ts +3 -0
  158. package/src/native-modules/natives/openURL.ts +40 -0
  159. package/src/react/index.ts +1 -0
  160. package/src/react/useWaitForReturnNavigator.ts +75 -0
  161. package/src/rn-polyfills/index.ts +7 -0
  162. package/src/rn-polyfills/symbol-asynciterator/index.ts +15 -0
  163. package/src/rn-polyfills/url/index.ts +1 -0
  164. package/src/router/Router.tsx +164 -0
  165. package/src/router/components/BackButton.tsx +58 -0
  166. package/src/router/components/CanGoBackGuard.tsx +31 -0
  167. package/src/router/components/RouterBackButton.tsx +32 -0
  168. package/src/router/components/StackNavigator.tsx +12 -0
  169. package/src/router/constants.ts +3 -0
  170. package/src/router/createRoute.test-d.ts +52 -0
  171. package/src/router/createRoute.ts +161 -0
  172. package/src/router/hooks/useInitialRouteName.tsx +22 -0
  173. package/src/router/hooks/useIsInitialScreen.ts +7 -0
  174. package/src/router/hooks/useRouterControls.tsx +72 -0
  175. package/src/router/index.ts +3 -0
  176. package/src/router/types/RequireContext.ts +7 -0
  177. package/src/router/types/RouteScreen.ts +17 -0
  178. package/src/router/types/Screen.tsx +24 -0
  179. package/src/router/types/index.ts +3 -0
  180. package/src/router/types/screen-option.ts +23 -0
  181. package/src/router/utils/createParentRouteScreenMap.spec.ts +166 -0
  182. package/src/router/utils/createParentRouteScreenMap.ts +136 -0
  183. package/src/router/utils/defaultParserParams.spec.ts +46 -0
  184. package/src/router/utils/defaultParserParams.ts +19 -0
  185. package/src/router/utils/index.ts +2 -0
  186. package/src/router/utils/matchers.ts +5 -0
  187. package/src/router/utils/mergeParentLayoutScreen.spec.tsx +112 -0
  188. package/src/router/utils/mergeParentLayoutScreen.tsx +43 -0
  189. package/src/router/utils/path.spec.ts +135 -0
  190. package/src/router/utils/path.ts +105 -0
  191. package/src/router/utils/screen.tsx +95 -0
  192. package/src/scroll-view-inertial-background/ScrollViewInertialBackground.tsx +99 -0
  193. package/src/scroll-view-inertial-background/index.ts +1 -0
  194. package/src/status-bar/StatusBar.android.tsx +36 -0
  195. package/src/status-bar/StatusBar.d.ts +4 -0
  196. package/src/status-bar/StatusBar.ios.tsx +34 -0
  197. package/src/status-bar/index.ts +2 -0
  198. package/src/status-bar/types.ts +21 -0
  199. package/src/status-bar/utils.ts +20 -0
  200. package/src/types/global.ts +21 -0
  201. package/src/use-back-event/index.ts +1 -0
  202. package/src/use-back-event/useBackEvent.tsx +260 -0
  203. package/src/utils/noop.ts +1 -0
  204. package/src/utils/usePreservedCallback.ts +16 -0
  205. package/src/video/Video.tsx +104 -0
  206. package/src/video/index.ts +1 -0
  207. package/src/video/instance.tsx +28 -0
  208. package/src/visibility/VisibilityProvider.tsx +36 -0
  209. package/src/visibility/index.ts +6 -0
  210. package/src/visibility/react-navigation/index.ts +2 -0
  211. package/src/visibility/react-navigation/useIsFocusedSafely.tsx +58 -0
  212. package/src/visibility/react-navigation/useNavigationSafely.tsx +30 -0
  213. package/src/visibility/useIsAppForeground.tsx +73 -0
  214. package/src/visibility/useVisibility.tsx +54 -0
  215. package/src/visibility/useVisibilityChange.ts +69 -0
  216. package/src/visibility/useVisibilityChanged.tsx +69 -0
  217. package/src/visibility/utils/usePrevious.tsx +24 -0
@@ -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
+ });
@@ -0,0 +1,136 @@
1
+ import { getFileNameFromPath } from './path';
2
+ import type { RouteScreen } from '../types/RouteScreen';
3
+
4
+ /**
5
+ * Extracts only the directory path from a given file path
6
+ * Example) './about/test' -> './about'
7
+ * Example) './about' -> '.' (default logic)
8
+ * but... For testing, if the path is included in 'layoutDirs', return as is
9
+ */
10
+ function getDirectoryPath(filePath: string, layoutDirs: Set<string>): string {
11
+ // If included in layoutDirs, this path itself is a directory-based route
12
+ if (layoutDirs.has(filePath)) {
13
+ return filePath;
14
+ }
15
+
16
+ const lastSlashIndex = filePath.lastIndexOf('/');
17
+ if (lastSlashIndex === -1) {
18
+ // If there's no slash, treat as '.'
19
+ return '.';
20
+ }
21
+ const dirPath = filePath.substring(0, lastSlashIndex);
22
+ return dirPath === '' ? '.' : dirPath;
23
+ }
24
+
25
+ /**
26
+ * Get parent directory path
27
+ * Example) './nested/deep' -> './nested'
28
+ * Example) './nested' -> '.'
29
+ * Example) '.' -> '' (no more parent)
30
+ */
31
+ function getParentDirectory(dirPath: string): string {
32
+ if (dirPath === '.' || dirPath === '') {
33
+ return '';
34
+ }
35
+ const idx = dirPath.lastIndexOf('/');
36
+ if (idx === -1) {
37
+ return '.';
38
+ }
39
+ const parent = dirPath.substring(0, idx);
40
+ return parent === '' ? '.' : parent;
41
+ }
42
+
43
+ /**
44
+ * @name createParentRouteScreenMap
45
+ * @description
46
+ * Finds a specific keyword in a given list of files and
47
+ * maps each page/layout to use its "nearest parent layout"
48
+ */
49
+ export function createParentRouteScreenMap(routeScreens: RouteScreen[], keyword: string): Map<string, RouteScreen> {
50
+ // 1) Find keyword files and record their directories in layoutDirs set
51
+ // Example) "./about/[keyword]" -> layoutDirs.add("./about")
52
+ const layoutDirs = new Set<string>();
53
+ const layoutScreens = new Map<string, RouteScreen>();
54
+
55
+ for (const screen of routeScreens) {
56
+ const fileName = getFileNameFromPath(screen.path, { withExtension: false });
57
+ if (fileName === keyword) {
58
+ const dirPath = screen.path.substring(0, screen.path.lastIndexOf('/'));
59
+ layoutDirs.add(dirPath || '.'); // Store directory path like './about'
60
+ }
61
+ }
62
+
63
+ // 2) Create layoutScreens (directory->keyword file) mapping
64
+ for (const screen of routeScreens) {
65
+ const fileName = getFileNameFromPath(screen.path, { withExtension: false });
66
+ if (fileName === keyword) {
67
+ // The directory this keyword file belongs to is already in layoutDirs
68
+ // dirPath: './about', './nested', etc.
69
+ const dirPath = getDirectoryPath(screen.path, layoutDirs);
70
+ layoutScreens.set(dirPath, screen);
71
+ }
72
+ }
73
+
74
+ /**
75
+ * 3) Function to find the nearest layout by traversing up parent directories
76
+ * - skipSameDir: whether to check from own directory or from parent
77
+ *
78
+ * memo: (dirPath + '|' + skipSameDir) -> RouteScreen | undefined
79
+ */
80
+ const memo = new Map<string, RouteScreen | undefined>();
81
+
82
+ function findNearestLayout(dir: string, skipSameDir = false): RouteScreen | undefined {
83
+ const cacheKey = dir + '|' + skipSameDir;
84
+ if (memo.has(cacheKey)) {
85
+ return memo.get(cacheKey);
86
+ }
87
+
88
+ let current = dir;
89
+
90
+ if (skipSameDir) {
91
+ current = getParentDirectory(current);
92
+ }
93
+
94
+ while (current) {
95
+ if (layoutScreens.has(current)) {
96
+ const foundLayout = layoutScreens.get(current);
97
+ memo.set(cacheKey, foundLayout);
98
+ return foundLayout;
99
+ }
100
+ if (current === '.' || current === '') {
101
+ break;
102
+ }
103
+ current = getParentDirectory(current);
104
+ }
105
+
106
+ memo.set(cacheKey, undefined);
107
+ return undefined;
108
+ }
109
+
110
+ // 4) Finally create "path -> parent layout" mapping
111
+ const parentRouteScreenMap = new Map<string, RouteScreen>();
112
+
113
+ for (const screen of routeScreens) {
114
+ const fileName = getFileNameFromPath(screen.path, { withExtension: false });
115
+
116
+ // Get the "directory" this file (or directory) belongs to
117
+ const dirPath = getDirectoryPath(screen.path, layoutDirs);
118
+
119
+ if (fileName === keyword) {
120
+ // (1) If this is a keyword file, search for layout in "parent directory"
121
+ // => must skip self (skipSameDir=true)
122
+ const parentLayout = findNearestLayout(dirPath, true);
123
+ if (parentLayout) {
124
+ parentRouteScreenMap.set(screen.path, parentLayout);
125
+ }
126
+ } else {
127
+ // (2) If it's a regular page (or regular file), search for layout from own directory up
128
+ const nearestLayout = findNearestLayout(dirPath, false);
129
+ if (nearestLayout) {
130
+ parentRouteScreenMap.set(screen.path, nearestLayout);
131
+ }
132
+ }
133
+ }
134
+
135
+ return parentRouteScreenMap;
136
+ }
@@ -0,0 +1,46 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { defaultParserParams } from './defaultParserParams';
3
+
4
+ describe('defaultParserParams', () => {
5
+ it('Parses numeric and boolean values from strings', () => {
6
+ const result = defaultParserParams({ foo: '123', bar: 'true' });
7
+ expect(result).toEqual({
8
+ foo: 123,
9
+ bar: true,
10
+ });
11
+ });
12
+
13
+ it('Parses parameters in query string format', () => {
14
+ const result = defaultParserParams({ foo: '123', bar: 'true' });
15
+ expect(result).toEqual({
16
+ foo: 123,
17
+ bar: true,
18
+ });
19
+ });
20
+
21
+ it('Maintains original value for unparseable strings', () => {
22
+ const result = defaultParserParams({ name: 'john', age: 'invalid' });
23
+ expect(result).toEqual({
24
+ name: 'john',
25
+ age: 'invalid',
26
+ });
27
+ });
28
+
29
+ it('Parses JSON format arrays', () => {
30
+ const result = defaultParserParams({ numbers: '[1,2,3]', strings: '["a","b"]' });
31
+ expect(result).toEqual({
32
+ numbers: [1, 2, 3],
33
+ strings: ['a', 'b'],
34
+ });
35
+ });
36
+
37
+ it('Parses JSON format objects', () => {
38
+ const result = defaultParserParams({ user: '{"name":"john","age":30}' });
39
+ expect(result).toEqual({
40
+ user: {
41
+ name: 'john',
42
+ age: 30,
43
+ },
44
+ });
45
+ });
46
+ });
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Parses URL query string and converts it to an object.
3
+ * @param params - Query string to parse
4
+ * @returns Parsed query object
5
+ * @example
6
+ * // Input: { foo: '123', bar: 'true' }
7
+ * // Output: { foo: 123, bar: true }
8
+ */
9
+ export function defaultParserParams(params?: Record<string, string>): Record<string, unknown> {
10
+ return Object.fromEntries(
11
+ Object.entries(params ?? {}).map(([key, value]) => {
12
+ try {
13
+ return [key, JSON.parse(value)];
14
+ } catch {
15
+ return [key, value];
16
+ }
17
+ })
18
+ );
19
+ }
@@ -0,0 +1,2 @@
1
+ export * from './screen';
2
+ export * from './defaultParserParams';
@@ -0,0 +1,5 @@
1
+ /** `[page]` -> `page` */
2
+ export function matchDynamicName(name: string): string | undefined {
3
+ // eslint-disable-next-line no-useless-escape
4
+ return name.match(/^\[([^[\](?:\.\.\.)]+?)\]$/)?.[1];
5
+ }
@@ -0,0 +1,112 @@
1
+ import { render } from '@testing-library/react';
2
+ import { ReactNode } from 'react';
3
+ import { it, describe, expect } from 'vitest';
4
+ import { Screen } from '..';
5
+ import { mergeParentLayoutScreen } from './mergeParentLayoutScreen';
6
+ import { RouteScreen } from '../types/RouteScreen';
7
+
8
+ declare global {
9
+ interface HTMLElement {
10
+ innerHTML: string;
11
+ }
12
+ }
13
+
14
+ function ItemLayout({ children }: { children: ReactNode }) {
15
+ return <div id="item-layout">{children}</div>;
16
+ }
17
+
18
+ function RootLayout({ children }: { children: ReactNode }) {
19
+ return <div id="root-layout">{children}</div>;
20
+ }
21
+
22
+ function SubItemLayout({ children }: { children: ReactNode }) {
23
+ return <div id="sub-item-layout">{children}</div>;
24
+ }
25
+
26
+ describe('mergeParentLayoutScreen', () => {
27
+ it('Parent layout and child layout should be nested in the correct order', () => {
28
+ const map = new Map<string, RouteScreen>();
29
+
30
+ map.set('./item/detail', {
31
+ component: ItemLayout as Screen,
32
+ path: './item/_layout',
33
+ });
34
+
35
+ map.set('./item/_layout', {
36
+ component: RootLayout as Screen,
37
+ path: './_layout',
38
+ });
39
+
40
+ const MergedLayout = mergeParentLayoutScreen(map, './item/detail');
41
+
42
+ const { container } = render(<MergedLayout>item detail</MergedLayout>);
43
+
44
+ // Should be nested in order: RootLayout > ItemLayout > content
45
+ expect(container.innerHTML).toBe(
46
+ render(
47
+ <div id="root-layout">
48
+ <div id="item-layout">item detail</div>
49
+ </div>
50
+ ).container.innerHTML
51
+ );
52
+ });
53
+
54
+ it('Triple nested layout should render in the correct order', () => {
55
+ const map = new Map<string, RouteScreen>();
56
+
57
+ map.set('./item/detail/sub', {
58
+ component: SubItemLayout as Screen,
59
+ path: './item/detail/_layout',
60
+ });
61
+
62
+ map.set('./item/detail/_layout', {
63
+ component: ItemLayout as Screen,
64
+ path: './item/_layout',
65
+ });
66
+
67
+ map.set('./item/_layout', {
68
+ component: RootLayout as Screen,
69
+ path: './_layout',
70
+ });
71
+
72
+ const MergedLayout = mergeParentLayoutScreen(map, './item/detail/sub');
73
+
74
+ const { container } = render(<MergedLayout>sub item detail</MergedLayout>);
75
+
76
+ // Should be nested in order: RootLayout > ItemLayout > SubItemLayout > content
77
+ expect(container.innerHTML).toBe(
78
+ render(
79
+ <div id="root-layout">
80
+ <div id="item-layout">
81
+ <div id="sub-item-layout">sub item detail</div>
82
+ </div>
83
+ </div>
84
+ ).container.innerHTML
85
+ );
86
+ });
87
+
88
+ it('Single layout should render correctly', () => {
89
+ const map = new Map<string, RouteScreen>();
90
+
91
+ map.set('./item/detail', {
92
+ component: RootLayout as Screen,
93
+ path: './_layout',
94
+ });
95
+
96
+ const MergedLayout = mergeParentLayoutScreen(map, './item/detail');
97
+
98
+ const { container } = render(<MergedLayout>item detail</MergedLayout>);
99
+
100
+ expect(container.innerHTML).toBe(render(<div id="root-layout">item detail</div>).container.innerHTML);
101
+ });
102
+
103
+ it('Should render only content when no layout is defined', () => {
104
+ const map = new Map<string, RouteScreen>();
105
+
106
+ const MergedLayout = mergeParentLayoutScreen(map, './item/detail');
107
+
108
+ const { container } = render(<MergedLayout>item detail</MergedLayout>);
109
+
110
+ expect(container.innerHTML).toBe(render(<>item detail</>).container.innerHTML);
111
+ });
112
+ });
@@ -0,0 +1,43 @@
1
+ import { FC, ReactNode } from 'react';
2
+ import { RouteScreen } from '../types/RouteScreen';
3
+
4
+ export type LayoutScreen = FC<{ children: ReactNode }>;
5
+
6
+ /**
7
+ * Starting from the layout mapped to a specific path, traverses up to parent layouts,
8
+ * creates an array in the order of "innermost → outermost",
9
+ * then builds up the final wrapper component using reduce.
10
+ *
11
+ * Example) map:
12
+ * './item/detail' => { component: ItemLayout, path: './item/_layout' }
13
+ * './item/_layout' => { component: RootLayout, path: './_layout' }
14
+ *
15
+ * => layoutChain = [ItemLayout, RootLayout]
16
+ * => Final rendering result: <RootLayout><ItemLayout>children</ItemLayout></RootLayout>
17
+ */
18
+ export function mergeParentLayoutScreen(screens: Map<string, RouteScreen>, path: string): LayoutScreen {
19
+ // Final wrapper component
20
+ let MergedLayout: FC<{ children?: ReactNode }> = ({ children }) => <>{children}</>;
21
+
22
+ let currentPath: string | undefined = path;
23
+ while (currentPath) {
24
+ const routeScreen = screens.get(currentPath);
25
+ if (!routeScreen) {
26
+ break;
27
+ }
28
+
29
+ // Wrap with immediate parent wrapper
30
+ const LayoutScreen = routeScreen.component as LayoutScreen;
31
+ const ChildLayout = MergedLayout;
32
+ MergedLayout = ({ children }) => (
33
+ <LayoutScreen>
34
+ <ChildLayout>{children}</ChildLayout>
35
+ </LayoutScreen>
36
+ );
37
+
38
+ // Move to parent path
39
+ currentPath = routeScreen.path;
40
+ }
41
+
42
+ return MergedLayout;
43
+ }