@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.
- package/CHANGELOG.md +66 -0
- 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/Granite.d.ts +60 -0
- package/dist/app/index.d.ts +2 -0
- package/dist/app/registerPage.d.ts +20 -0
- package/dist/async-bridges.d.ts +2 -0
- package/dist/constant-bridges.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 +17 -0
- package/dist/initial-props/InitialProps.d.ts +127 -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 +1 -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 +53 -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/types/global.d.ts +18 -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/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 +92 -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/Granite.tsx +130 -0
- package/src/app/index.ts +2 -0
- package/src/app/registerPage.ts +29 -0
- package/src/async-bridges.ts +2 -0
- package/src/constant-bridges.ts +1 -0
- package/src/dev-entrypoint/index.tsx +21 -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 +19 -0
- package/src/initial-props/InitialProps.ts +144 -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 +1 -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 +111 -0
- package/src/scroll-view-inertial-background/ScrollViewInertialBackground.tsx +99 -0
- package/src/scroll-view-inertial-background/index.ts +1 -0
- package/src/types/global.ts +31 -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/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,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
|
+
}
|
|
@@ -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
|
+
}
|