@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.
- package/LICENSE +202 -0
- package/README.md +24 -0
- package/bin/cli.js +3 -0
- package/cli.d.ts +1 -0
- package/cli.js +4 -0
- package/config.d.ts +2 -0
- package/config.js +5 -0
- package/dist/app/App/index.android.d.ts +2 -0
- package/dist/app/App/index.ios.d.ts +6 -0
- package/dist/app/AppRoot.d.ts +1 -0
- package/dist/app/Granite.d.ts +61 -0
- package/dist/app/HostAppRoot.d.ts +1 -0
- package/dist/app/index.d.ts +2 -0
- package/dist/async-bridges.d.ts +2 -0
- package/dist/blur/BlurView.d.ts +78 -0
- package/dist/blur/ReactNativeBlurModule.d.ts +6 -0
- package/dist/blur/constants.d.ts +1 -0
- package/dist/blur/index.d.ts +1 -0
- package/dist/constant-bridges.d.ts +1 -0
- package/dist/constants.d.ts +1 -0
- package/dist/dev-entrypoint/index.d.ts +2 -0
- package/dist/event/abstract.d.ts +42 -0
- package/dist/event/index.d.ts +2 -0
- package/dist/event/useGraniteEvent.d.ts +14 -0
- package/dist/impression-area/ImpressionArea.d.ts +231 -0
- package/dist/impression-area/index.d.ts +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/initial-props/InitialProps.d.ts +92 -0
- package/dist/initial-props/index.d.ts +1 -0
- package/dist/intersection-observer/IOContext.d.ts +10 -0
- package/dist/intersection-observer/IOFlatList.d.ts +55 -0
- package/dist/intersection-observer/IOManager.d.ts +24 -0
- package/dist/intersection-observer/IOScrollView.d.ts +59 -0
- package/dist/intersection-observer/InView.d.ts +107 -0
- package/dist/intersection-observer/IntersectionObserver.d.ts +67 -0
- package/dist/intersection-observer/index.d.ts +8 -0
- package/dist/intersection-observer/withIO.d.ts +20 -0
- package/dist/jest/index.d.ts +1 -0
- package/dist/jest/index.js +32 -0
- package/dist/keyboard/KeyboardAboveView.d.ts +40 -0
- package/dist/keyboard/index.d.ts +2 -0
- package/dist/keyboard/useKeyboardAnimatedHeight.d.ts +20 -0
- package/dist/native-event-emitter/eventEmitters/index.d.ts +2 -0
- package/dist/native-event-emitter/eventEmitters/types.d.ts +4 -0
- package/dist/native-event-emitter/eventEmitters/visibilityChanged.d.ts +10 -0
- package/dist/native-event-emitter/index.d.ts +1 -0
- package/dist/native-event-emitter/nativeEventEmitter.d.ts +15 -0
- package/dist/native-modules/core/GraniteCoreModule.d.ts +8 -0
- package/dist/native-modules/index.d.ts +3 -0
- package/dist/native-modules/natives/GraniteModule.d.ts +7 -0
- package/dist/native-modules/natives/closeView.d.ts +21 -0
- package/dist/native-modules/natives/getSchemeUri.d.ts +23 -0
- package/dist/native-modules/natives/index.d.ts +3 -0
- package/dist/native-modules/natives/openURL.d.ts +36 -0
- package/dist/react/index.d.ts +1 -0
- package/dist/react/useWaitForReturnNavigator.d.ts +39 -0
- package/dist/rn-polyfills/index.d.ts +1 -0
- package/dist/rn-polyfills/symbol-asynciterator/index.d.ts +9 -0
- package/dist/rn-polyfills/url/index.d.ts +1 -0
- package/dist/router/Router.d.ts +59 -0
- package/dist/router/components/BackButton.d.ts +7 -0
- package/dist/router/components/CanGoBackGuard.d.ts +6 -0
- package/dist/router/components/RouterBackButton.d.ts +9 -0
- package/dist/router/components/StackNavigator.d.ts +54 -0
- package/dist/router/constants.d.ts +2 -0
- package/dist/router/createRoute.d.ts +39 -0
- package/dist/router/createRoute.test-d.d.ts +9 -0
- package/dist/router/hooks/useInitialRouteName.d.ts +1 -0
- package/dist/router/hooks/useIsInitialScreen.d.ts +1 -0
- package/dist/router/hooks/useRouterControls.d.ts +11 -0
- package/dist/router/index.d.ts +3 -0
- package/dist/router/types/RequireContext.d.ts +7 -0
- package/dist/router/types/RouteScreen.d.ts +16 -0
- package/dist/router/types/Screen.d.ts +23 -0
- package/dist/router/types/index.d.ts +3 -0
- package/dist/router/types/screen-option.d.ts +4 -0
- package/dist/router/utils/createParentRouteScreenMap.d.ts +8 -0
- package/dist/router/utils/defaultParserParams.d.ts +9 -0
- package/dist/router/utils/index.d.ts +2 -0
- package/dist/router/utils/matchers.d.ts +2 -0
- package/dist/router/utils/mergeParentLayoutScreen.d.ts +18 -0
- package/dist/router/utils/path.d.ts +53 -0
- package/dist/router/utils/screen.d.ts +37 -0
- package/dist/scroll-view-inertial-background/ScrollViewInertialBackground.d.ts +49 -0
- package/dist/scroll-view-inertial-background/index.d.ts +1 -0
- package/dist/status-bar/StatusBar.android.d.ts +3 -0
- package/dist/status-bar/StatusBar.ios.d.ts +3 -0
- package/dist/status-bar/index.d.ts +2 -0
- package/dist/status-bar/types.d.ts +20 -0
- package/dist/status-bar/utils.d.ts +3 -0
- package/dist/types/global.d.ts +14 -0
- package/dist/use-back-event/index.d.ts +1 -0
- package/dist/use-back-event/useBackEvent.d.ts +135 -0
- package/dist/utils/noop.d.ts +1 -0
- package/dist/utils/usePreservedCallback.d.ts +1 -0
- package/dist/video/Video.d.ts +67 -0
- package/dist/video/index.d.ts +1 -0
- package/dist/video/instance.d.ts +9 -0
- package/dist/visibility/VisibilityProvider.d.ts +27 -0
- package/dist/visibility/index.d.ts +6 -0
- package/dist/visibility/react-navigation/index.d.ts +2 -0
- package/dist/visibility/react-navigation/useIsFocusedSafely.d.ts +20 -0
- package/dist/visibility/react-navigation/useNavigationSafely.d.ts +19 -0
- package/dist/visibility/useIsAppForeground.d.ts +39 -0
- package/dist/visibility/useVisibility.d.ts +35 -0
- package/dist/visibility/useVisibilityChange.d.ts +51 -0
- package/dist/visibility/useVisibilityChanged.d.ts +41 -0
- package/dist/visibility/utils/usePrevious.d.ts +15 -0
- package/jest.d.ts +1 -0
- package/package.json +94 -0
- package/presets.d.ts +1 -0
- package/src/app/App/index.android.tsx +6 -0
- package/src/app/App/index.d.ts +6 -0
- package/src/app/App/index.ios.tsx +13 -0
- package/src/app/AppRoot.tsx +39 -0
- package/src/app/Granite.tsx +128 -0
- package/src/app/HostAppRoot.tsx +19 -0
- package/src/app/index.ts +2 -0
- package/src/async-bridges.ts +2 -0
- package/src/blur/BlurView.tsx +103 -0
- package/src/blur/ReactNativeBlurModule.ts +19 -0
- package/src/blur/constants.ts +3 -0
- package/src/blur/index.ts +1 -0
- package/src/constant-bridges.ts +1 -0
- package/src/constants.ts +1 -0
- package/src/dev-entrypoint/index.tsx +17 -0
- package/src/event/abstract.ts +130 -0
- package/src/event/index.ts +2 -0
- package/src/event/useGraniteEvent.ts +34 -0
- package/src/impression-area/ImpressionArea.tsx +341 -0
- package/src/impression-area/index.ts +1 -0
- package/src/index.ts +24 -0
- package/src/initial-props/InitialProps.ts +95 -0
- package/src/initial-props/index.ts +1 -0
- package/src/intersection-observer/IOContext.ts +16 -0
- package/src/intersection-observer/IOFlatList.ts +72 -0
- package/src/intersection-observer/IOManager.ts +73 -0
- package/src/intersection-observer/IOScrollView.ts +69 -0
- package/src/intersection-observer/InView.tsx +205 -0
- package/src/intersection-observer/IntersectionObserver.ts +212 -0
- package/src/intersection-observer/index.ts +24 -0
- package/src/intersection-observer/withIO.tsx +151 -0
- package/src/jest/index.ts +1 -0
- package/src/keyboard/KeyboardAboveView.tsx +62 -0
- package/src/keyboard/index.ts +2 -0
- package/src/keyboard/useKeyboardAnimatedHeight.tsx +81 -0
- package/src/native-event-emitter/eventEmitters/index.ts +3 -0
- package/src/native-event-emitter/eventEmitters/types.ts +4 -0
- package/src/native-event-emitter/eventEmitters/visibilityChanged.ts +11 -0
- package/src/native-event-emitter/index.ts +1 -0
- package/src/native-event-emitter/nativeEventEmitter.ts +18 -0
- package/src/native-modules/core/GraniteCoreModule.ts +9 -0
- package/src/native-modules/index.ts +3 -0
- package/src/native-modules/natives/GraniteModule.ts +8 -0
- package/src/native-modules/natives/closeView.ts +25 -0
- package/src/native-modules/natives/getSchemeUri.ts +27 -0
- package/src/native-modules/natives/index.ts +3 -0
- package/src/native-modules/natives/openURL.ts +40 -0
- package/src/react/index.ts +1 -0
- package/src/react/useWaitForReturnNavigator.ts +75 -0
- package/src/rn-polyfills/index.ts +7 -0
- package/src/rn-polyfills/symbol-asynciterator/index.ts +15 -0
- package/src/rn-polyfills/url/index.ts +1 -0
- package/src/router/Router.tsx +164 -0
- package/src/router/components/BackButton.tsx +58 -0
- package/src/router/components/CanGoBackGuard.tsx +31 -0
- package/src/router/components/RouterBackButton.tsx +32 -0
- package/src/router/components/StackNavigator.tsx +12 -0
- package/src/router/constants.ts +3 -0
- package/src/router/createRoute.test-d.ts +52 -0
- package/src/router/createRoute.ts +161 -0
- package/src/router/hooks/useInitialRouteName.tsx +22 -0
- package/src/router/hooks/useIsInitialScreen.ts +7 -0
- package/src/router/hooks/useRouterControls.tsx +72 -0
- package/src/router/index.ts +3 -0
- package/src/router/types/RequireContext.ts +7 -0
- package/src/router/types/RouteScreen.ts +17 -0
- package/src/router/types/Screen.tsx +24 -0
- package/src/router/types/index.ts +3 -0
- package/src/router/types/screen-option.ts +23 -0
- package/src/router/utils/createParentRouteScreenMap.spec.ts +166 -0
- package/src/router/utils/createParentRouteScreenMap.ts +136 -0
- package/src/router/utils/defaultParserParams.spec.ts +46 -0
- package/src/router/utils/defaultParserParams.ts +19 -0
- package/src/router/utils/index.ts +2 -0
- package/src/router/utils/matchers.ts +5 -0
- package/src/router/utils/mergeParentLayoutScreen.spec.tsx +112 -0
- package/src/router/utils/mergeParentLayoutScreen.tsx +43 -0
- package/src/router/utils/path.spec.ts +135 -0
- package/src/router/utils/path.ts +105 -0
- package/src/router/utils/screen.tsx +95 -0
- package/src/scroll-view-inertial-background/ScrollViewInertialBackground.tsx +99 -0
- package/src/scroll-view-inertial-background/index.ts +1 -0
- package/src/status-bar/StatusBar.android.tsx +36 -0
- package/src/status-bar/StatusBar.d.ts +4 -0
- package/src/status-bar/StatusBar.ios.tsx +34 -0
- package/src/status-bar/index.ts +2 -0
- package/src/status-bar/types.ts +21 -0
- package/src/status-bar/utils.ts +20 -0
- package/src/types/global.ts +21 -0
- package/src/use-back-event/index.ts +1 -0
- package/src/use-back-event/useBackEvent.tsx +260 -0
- package/src/utils/noop.ts +1 -0
- package/src/utils/usePreservedCallback.ts +16 -0
- package/src/video/Video.tsx +104 -0
- package/src/video/index.ts +1 -0
- package/src/video/instance.tsx +28 -0
- package/src/visibility/VisibilityProvider.tsx +36 -0
- package/src/visibility/index.ts +6 -0
- package/src/visibility/react-navigation/index.ts +2 -0
- package/src/visibility/react-navigation/useIsFocusedSafely.tsx +58 -0
- package/src/visibility/react-navigation/useNavigationSafely.tsx +30 -0
- package/src/visibility/useIsAppForeground.tsx +73 -0
- package/src/visibility/useVisibility.tsx +54 -0
- package/src/visibility/useVisibilityChange.ts +69 -0
- package/src/visibility/useVisibilityChanged.tsx +69 -0
- 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,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
|
+
}
|