@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,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
+ }
@@ -0,0 +1,135 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ excludeDynamicNamePattern,
4
+ excludeFileExtension,
5
+ excludeRelativePath,
6
+ getFileNameFromPath,
7
+ getRoutePath,
8
+ } from './path';
9
+
10
+ describe('excludeFileExtension', () => {
11
+ it('"./index.ts" is changed to "./index"', () => {
12
+ expect(excludeFileExtension('./index.ts')).toEqual('./index');
13
+ });
14
+
15
+ it('"./index.js" is changed to "./index"', () => {
16
+ expect(excludeFileExtension('./index.js')).toEqual('./index');
17
+ });
18
+
19
+ it('"./list/detail.tsx" is changed to "./list/detail"', () => {
20
+ expect(excludeFileExtension('./list/detail.tsx')).toEqual('./list/detail');
21
+ });
22
+
23
+ it('"./list/detail.jsx" is changed to "./list/detail"', () => {
24
+ expect(excludeFileExtension('./list/detail.jsx')).toEqual('./list/detail');
25
+ });
26
+
27
+ it('"./detail.foo" remains as "./detail.foo"', () => {
28
+ expect(excludeFileExtension('./detail.foo')).toEqual('./detail.foo');
29
+ });
30
+ });
31
+
32
+ describe('excludeRelativePath', () => {
33
+ it('"./index.tsx" is changed to "index.tsx"', () => {
34
+ expect(excludeRelativePath('./index.tsx')).toEqual('index.tsx');
35
+ });
36
+
37
+ it('"./../index.tsx" is changed to "index.tsx"', () => {
38
+ expect(excludeRelativePath('./../index.tsx')).toEqual('index.tsx');
39
+ });
40
+
41
+ it('"../../index.tsx" is changed to "index.tsx"', () => {
42
+ expect(excludeRelativePath('../../index.tsx')).toEqual('index.tsx');
43
+ });
44
+ });
45
+
46
+ describe('excludeDynamicNamePattern', () => {
47
+ it('"[id]" is changed to "id"', () => {
48
+ expect(excludeDynamicNamePattern('[id]')).toEqual('id');
49
+ });
50
+
51
+ it('"[id]/[name]" is changed to "id/name"', () => {
52
+ expect(excludeDynamicNamePattern('[id]/[name]')).toEqual('id/name');
53
+ });
54
+ });
55
+
56
+ describe('getRoutePath', () => {
57
+ describe('posix', () => {
58
+ it('"/index.tsx" is converted to "/"', () => {
59
+ expect(getRoutePath('/index.tsx')).toEqual('/');
60
+ });
61
+
62
+ it('"/index.ts" is converted to "/"', () => {
63
+ expect(getRoutePath('/index.tsx')).toEqual('/');
64
+ });
65
+
66
+ it('"/list/index.tsx" is converted to "/list"', () => {
67
+ expect(getRoutePath('/list/index.tsx')).toEqual('/list');
68
+ });
69
+
70
+ it('"/list/detail.tsx" is converted to "/list"', () => {
71
+ expect(getRoutePath('/list/detail.tsx')).toEqual('/list/detail');
72
+ });
73
+
74
+ it('"/list/[id].tsx" is converted to "/:id"', () => {
75
+ expect(getRoutePath('/list/[id].tsx')).toEqual('/list/:id');
76
+ });
77
+
78
+ it('"/list/[id].js" is converted to "/:id"', () => {
79
+ expect(getRoutePath('/list/[id].js')).toEqual('/list/:id');
80
+ });
81
+ });
82
+
83
+ describe('window', () => {
84
+ it('"\\index.tsx" is converted to "/"', () => {
85
+ expect(getRoutePath('\\index.tsx')).toEqual('/');
86
+ });
87
+
88
+ it('"\\index.ts" is converted to "/"', () => {
89
+ expect(getRoutePath('\\index.tsx')).toEqual('/');
90
+ });
91
+
92
+ it('"\\list\\index.tsx" is converted to "/list"', () => {
93
+ expect(getRoutePath('\\list\\index.tsx')).toEqual('/list');
94
+ });
95
+
96
+ it('"\\list\\detail.tsx" is converted to "/list"', () => {
97
+ expect(getRoutePath('\\list\\detail.tsx')).toEqual('/list/detail');
98
+ });
99
+
100
+ it('"\\list\\[id].tsx" is converted to "/:id"', () => {
101
+ expect(getRoutePath('\\list\\[id].tsx')).toEqual('/list/:id');
102
+ });
103
+
104
+ it('"\\list\\[id].js" is converted to "/:id"', () => {
105
+ expect(getRoutePath('\\list\\[id].js')).toEqual('/list/:id');
106
+ });
107
+ });
108
+ });
109
+
110
+ describe('getFileNameFromPath', () => {
111
+ it('extracts filename from file path', () => {
112
+ const result = getFileNameFromPath('/path/to/file.txt');
113
+ expect(result).toBe('file.txt');
114
+ });
115
+
116
+ it('returns the filename when path is just a filename', () => {
117
+ const result = getFileNameFromPath('file.txt');
118
+ expect(result).toBe('file.txt');
119
+ });
120
+
121
+ it('returns empty string when path is empty', () => {
122
+ const result = getFileNameFromPath('');
123
+ expect(result).toBe('');
124
+ });
125
+
126
+ it('returns empty string when path ends with slash', () => {
127
+ const result = getFileNameFromPath('/path/to/directory/');
128
+ expect(result).toBe('');
129
+ });
130
+
131
+ it('extracts filename without extension using withExtension option', () => {
132
+ const result = getFileNameFromPath('/path/to/file.txt', { withExtension: false });
133
+ expect(result).toBe('file');
134
+ });
135
+ });
@@ -0,0 +1,105 @@
1
+ import { matchDynamicName } from './matchers';
2
+
3
+ /**
4
+ * Converts Windows-style paths to Posix-based paths.
5
+ *
6
+ * @example
7
+ * ```ts
8
+ * toPosixBasedPath(".\\index.tsx") // "./index.tsx"
9
+ * toPosixBasedPath("..\\..\\index.tsx") // "../../index.tsx"
10
+ * ```
11
+ */
12
+ function toPosixBasedPath(filePath: string): string {
13
+ return filePath.replace(/\\/g, '/');
14
+ }
15
+
16
+ /**
17
+ * @name excludeFileExtension
18
+ * @description Removes file extension.
19
+ */
20
+ export function excludeFileExtension(name: string): string {
21
+ return name.replace(/\.(tsx|jsx|ts|js)$/g, '');
22
+ }
23
+
24
+ /**
25
+ * @name excludeRelativePath
26
+ * @description Removes relative path from path.
27
+ * @example
28
+ * ```ts
29
+ * excludeRelativePath("./index.tsx") // "index.tsx"
30
+ * excludeRelativePath("./list/detail.tsx") // "list/detail.tsx"
31
+ * excludeRelativePath("../../index.tsx") // "index.tsx"
32
+ * ```
33
+ */
34
+ export function excludeRelativePath(filePath: string): string {
35
+ return filePath.replace(/^(?:\.\.?\/)+/g, '');
36
+ }
37
+
38
+ /**
39
+ * @name excludeDynamicNamePattern
40
+ * @description Removes `[` `]` from dynamic route pattern.
41
+ * @example
42
+ * ```ts
43
+ * excludeDynamicNamePattern("[id]") // "id"
44
+ * excludeDynamicNamePattern("[id]/[name]") // "id/name"
45
+ * ```
46
+ */
47
+ export function excludeDynamicNamePattern(filePath: string): string {
48
+ return filePath.replace(/\[|\]/g, '');
49
+ }
50
+
51
+ /**
52
+ * @name getRoutePath
53
+ * @description Converts to route path.
54
+ * @example
55
+ * ```ts
56
+ * getRoutePath('./index.tsx') // "/"
57
+ * getRoutePath('./list/index.tsx') // "/list"
58
+ * getRoutePath('./list/detail.tsx') // "/list/detail"
59
+ * getRoutePath('./list/[id].js') // "/list/:id"
60
+ * ```
61
+ */
62
+ export function getRoutePath(filePath: string): string {
63
+ const posixBasedPath = toPosixBasedPath(filePath);
64
+ const normalPath = excludeRelativePath(excludeFileExtension(posixBasedPath));
65
+
66
+ const routePath = normalPath
67
+ .split('/')
68
+ .map((segment) => {
69
+ if (segment === 'index') {
70
+ return '';
71
+ }
72
+
73
+ if (matchDynamicName(segment)) {
74
+ return `:${excludeDynamicNamePattern(segment)}`;
75
+ }
76
+
77
+ return segment;
78
+ })
79
+ .filter((segment) => segment.length > 0)
80
+ .join('/');
81
+
82
+ return '/' + routePath;
83
+ }
84
+
85
+ /**
86
+ * @name getFileNameFromPath
87
+ * @description Extracts filename from file path.
88
+ * @example
89
+ * ```ts
90
+ * getFileNameFromPath('/path/to/file.txt') // "file.txt"
91
+ * getFileNameFromPath('file.txt') // "file.txt"
92
+ * getFileNameFromPath('') // ""
93
+ * getFileNameFromPath('/path/to/directory/') // ""
94
+ * getFileNameFromPath('/path/to/file.txt', { withExtension: false }) // "file"
95
+ * ```
96
+ */
97
+ export function getFileNameFromPath(
98
+ filePath: string,
99
+ options: { withExtension?: boolean } = {
100
+ withExtension: true,
101
+ }
102
+ ): string {
103
+ const fileName = filePath.split('/').pop() || '';
104
+ return options.withExtension ? fileName : fileName.replace(/\.[^.]+$/g, '');
105
+ }