@react-navigation/core 8.0.0-alpha.4 → 8.0.0-alpha.41
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/README.md +17 -14
- package/lib/module/BaseNavigationContainer.js +173 -90
- package/lib/module/BaseNavigationContainer.js.map +1 -1
- package/lib/module/ConsumedParamsContext.js +5 -0
- package/lib/module/ConsumedParamsContext.js.map +1 -0
- package/lib/module/DataLoading.js +129 -0
- package/lib/module/DataLoading.js.map +1 -0
- package/lib/module/NavigationBuilderContext.js +7 -10
- package/lib/module/NavigationBuilderContext.js.map +1 -1
- package/lib/module/NavigationIndependentTree.js +54 -9
- package/lib/module/NavigationIndependentTree.js.map +1 -1
- package/lib/module/NavigationProvider.js +37 -12
- package/lib/module/NavigationProvider.js.map +1 -1
- package/lib/module/NavigationRootContext.js +8 -0
- package/lib/module/NavigationRootContext.js.map +1 -0
- package/lib/module/NavigationStateContext.js +3 -0
- package/lib/module/NavigationStateContext.js.map +1 -1
- package/lib/module/PreventRemoveContext.js.map +1 -1
- package/lib/module/PreventRemoveProvider.js +40 -31
- package/lib/module/PreventRemoveProvider.js.map +1 -1
- package/lib/module/SceneView.js +29 -57
- package/lib/module/SceneView.js.map +1 -1
- package/lib/module/StaticNavigation.js +219 -67
- package/lib/module/StaticNavigation.js.map +1 -1
- package/lib/module/StaticTreeContext.js +5 -0
- package/lib/module/StaticTreeContext.js.map +1 -0
- package/lib/module/createNavigationContainerRef.js +7 -9
- package/lib/module/createNavigationContainerRef.js.map +1 -1
- package/lib/module/createNavigatorFactory.js +27 -5
- package/lib/module/createNavigatorFactory.js.map +1 -1
- package/lib/module/findFocusedRoute.js +7 -4
- package/lib/module/findFocusedRoute.js.map +1 -1
- package/lib/module/getActionFromState.js +45 -20
- package/lib/module/getActionFromState.js.map +1 -1
- package/lib/module/getPathFromState.js +204 -94
- package/lib/module/getPathFromState.js.map +1 -1
- package/lib/module/getPatternParts.js +61 -14
- package/lib/module/getPatternParts.js.map +1 -1
- package/lib/module/getStateFromPath.js +608 -272
- package/lib/module/getStateFromPath.js.map +1 -1
- package/lib/module/getStateFromRouteParams.js +23 -0
- package/lib/module/getStateFromRouteParams.js.map +1 -0
- package/lib/module/index.js +7 -6
- package/lib/module/index.js.map +1 -1
- package/lib/module/internal.js +2 -0
- package/lib/module/internal.js.map +1 -0
- package/lib/module/isRecordEqual.js +1 -1
- package/lib/module/isRecordEqual.js.map +1 -1
- package/lib/module/queryString.js +71 -0
- package/lib/module/queryString.js.map +1 -0
- package/lib/module/theming/useTheme.js +1 -1
- package/lib/module/theming/useTheme.js.map +1 -1
- package/lib/module/types.js +35 -1
- package/lib/module/types.js.map +1 -1
- package/lib/module/useDescriptors.js +65 -25
- package/lib/module/useDescriptors.js.map +1 -1
- package/lib/module/useEventEmitter.js +48 -33
- package/lib/module/useEventEmitter.js.map +1 -1
- package/lib/module/useFocusEffect.js +4 -2
- package/lib/module/useFocusEffect.js.map +1 -1
- package/lib/module/useFocusEvents.js +15 -5
- package/lib/module/useFocusEvents.js.map +1 -1
- package/lib/module/useFocusedListenersChildrenAdapter.js +3 -3
- package/lib/module/useFocusedListenersChildrenAdapter.js.map +1 -1
- package/lib/module/useIsFocused.js +8 -12
- package/lib/module/useIsFocused.js.map +1 -1
- package/lib/module/useKeyedChildListeners.js +5 -2
- package/lib/module/useKeyedChildListeners.js.map +1 -1
- package/lib/module/useLazyValue.js.map +1 -1
- package/lib/module/useNavigation.js +4 -17
- package/lib/module/useNavigation.js.map +1 -1
- package/lib/module/useNavigationBuilder.js +244 -147
- package/lib/module/useNavigationBuilder.js.map +1 -1
- package/lib/module/useNavigationCache.js +92 -89
- package/lib/module/useNavigationCache.js.map +1 -1
- package/lib/module/useNavigationHelpers.js +22 -29
- package/lib/module/useNavigationHelpers.js.map +1 -1
- package/lib/module/useNavigationIndependentTree.js +1 -1
- package/lib/module/useNavigationIndependentTree.js.map +1 -1
- package/lib/module/useNavigationState.js +158 -46
- package/lib/module/useNavigationState.js.map +1 -1
- package/lib/module/useOnAction.js +58 -11
- package/lib/module/useOnAction.js.map +1 -1
- package/lib/module/useOnGetNavigation.js +26 -0
- package/lib/module/useOnGetNavigation.js.map +1 -0
- package/lib/module/useOnGetState.js +7 -4
- package/lib/module/useOnGetState.js.map +1 -1
- package/lib/module/useOnPreventRemove.js +40 -31
- package/lib/module/useOnPreventRemove.js.map +1 -1
- package/lib/module/useOnRouteFocus.js +2 -2
- package/lib/module/useOnRouteFocus.js.map +1 -1
- package/lib/module/useOptionsGetters.js +35 -27
- package/lib/module/useOptionsGetters.js.map +1 -1
- package/lib/module/usePreventRemove.js +11 -5
- package/lib/module/usePreventRemove.js.map +1 -1
- package/lib/module/usePreventRemoveContext.js +2 -2
- package/lib/module/usePreventRemoveContext.js.map +1 -1
- package/lib/module/useRegisterNavigator.js +3 -4
- package/lib/module/useRegisterNavigator.js.map +1 -1
- package/lib/module/useRoute.js +2 -0
- package/lib/module/useRoute.js.map +1 -1
- package/lib/module/useRouteCache.js +51 -35
- package/lib/module/useRouteCache.js.map +1 -1
- package/lib/module/useScheduleUpdate.js +12 -10
- package/lib/module/useScheduleUpdate.js.map +1 -1
- package/lib/module/useStateForPath.js +1 -1
- package/lib/module/useStateForPath.js.map +1 -1
- package/lib/module/useSyncState.js +37 -15
- package/lib/module/useSyncState.js.map +1 -1
- package/lib/module/validatePathConfig.js +50 -36
- package/lib/module/validatePathConfig.js.map +1 -1
- package/lib/typescript/src/BaseNavigationContainer.d.ts +6 -3
- package/lib/typescript/src/BaseNavigationContainer.d.ts.map +1 -1
- package/lib/typescript/src/ConsumedParamsContext.d.ts +3 -0
- package/lib/typescript/src/ConsumedParamsContext.d.ts.map +1 -0
- package/lib/typescript/src/DataLoading.d.ts +21 -0
- package/lib/typescript/src/DataLoading.d.ts.map +1 -0
- package/lib/typescript/src/EnsureSingleNavigator.d.ts +1 -1
- package/lib/typescript/src/EnsureSingleNavigator.d.ts.map +1 -1
- package/lib/typescript/src/Group.d.ts +1 -1
- package/lib/typescript/src/Group.d.ts.map +1 -1
- package/lib/typescript/src/NavigationBuilderContext.d.ts +22 -14
- package/lib/typescript/src/NavigationBuilderContext.d.ts.map +1 -1
- package/lib/typescript/src/NavigationContainerRefContext.d.ts +1 -1
- package/lib/typescript/src/NavigationContainerRefContext.d.ts.map +1 -1
- package/lib/typescript/src/NavigationFocusedRouteStateContext.d.ts +4 -4
- package/lib/typescript/src/NavigationFocusedRouteStateContext.d.ts.map +1 -1
- package/lib/typescript/src/NavigationHelpersContext.d.ts +1 -1
- package/lib/typescript/src/NavigationHelpersContext.d.ts.map +1 -1
- package/lib/typescript/src/NavigationIndependentTree.d.ts +1 -1
- package/lib/typescript/src/NavigationIndependentTree.d.ts.map +1 -1
- package/lib/typescript/src/NavigationProvider.d.ts +21 -18
- package/lib/typescript/src/NavigationProvider.d.ts.map +1 -1
- package/lib/typescript/src/NavigationRootContext.d.ts +8 -0
- package/lib/typescript/src/NavigationRootContext.d.ts.map +1 -0
- package/lib/typescript/src/NavigationStateContext.d.ts +3 -3
- package/lib/typescript/src/NavigationStateContext.d.ts.map +1 -1
- package/lib/typescript/src/PreventRemoveContext.d.ts +1 -0
- package/lib/typescript/src/PreventRemoveContext.d.ts.map +1 -1
- package/lib/typescript/src/PreventRemoveProvider.d.ts +1 -1
- package/lib/typescript/src/PreventRemoveProvider.d.ts.map +1 -1
- package/lib/typescript/src/SceneView.d.ts +5 -2
- package/lib/typescript/src/SceneView.d.ts.map +1 -1
- package/lib/typescript/src/Screen.d.ts +1 -1
- package/lib/typescript/src/Screen.d.ts.map +1 -1
- package/lib/typescript/src/StaticContainer.d.ts +1 -1
- package/lib/typescript/src/StaticContainer.d.ts.map +1 -1
- package/lib/typescript/src/StaticNavigation.d.ts +189 -111
- package/lib/typescript/src/StaticNavigation.d.ts.map +1 -1
- package/lib/typescript/src/StaticTreeContext.d.ts +4 -0
- package/lib/typescript/src/StaticTreeContext.d.ts.map +1 -0
- package/lib/typescript/src/createNavigationContainerRef.d.ts +1 -1
- package/lib/typescript/src/createNavigationContainerRef.d.ts.map +1 -1
- package/lib/typescript/src/createNavigatorFactory.d.ts +23 -5
- package/lib/typescript/src/createNavigatorFactory.d.ts.map +1 -1
- package/lib/typescript/src/findFocusedRoute.d.ts +3 -8
- package/lib/typescript/src/findFocusedRoute.d.ts.map +1 -1
- package/lib/typescript/src/getActionFromState.d.ts +4 -4
- package/lib/typescript/src/getActionFromState.d.ts.map +1 -1
- package/lib/typescript/src/getPathFromState.d.ts +3 -3
- package/lib/typescript/src/getPathFromState.d.ts.map +1 -1
- package/lib/typescript/src/getPatternParts.d.ts +5 -2
- package/lib/typescript/src/getPatternParts.d.ts.map +1 -1
- package/lib/typescript/src/getStateFromPath.d.ts +4 -7
- package/lib/typescript/src/getStateFromPath.d.ts.map +1 -1
- package/lib/typescript/src/getStateFromRouteParams.d.ts +3 -0
- package/lib/typescript/src/getStateFromRouteParams.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +36 -33
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/internal.d.ts +3 -0
- package/lib/typescript/src/internal.d.ts.map +1 -0
- package/lib/typescript/src/isRecordEqual.d.ts.map +1 -1
- package/lib/typescript/src/queryString.d.ts +5 -0
- package/lib/typescript/src/queryString.d.ts.map +1 -0
- package/lib/typescript/src/theming/ThemeContext.d.ts +1 -1
- package/lib/typescript/src/theming/ThemeContext.d.ts.map +1 -1
- package/lib/typescript/src/theming/ThemeProvider.d.ts +2 -2
- package/lib/typescript/src/theming/ThemeProvider.d.ts.map +1 -1
- package/lib/typescript/src/theming/useTheme.d.ts +1 -1
- package/lib/typescript/src/theming/useTheme.d.ts.map +1 -1
- package/lib/typescript/src/types.d.ts +265 -167
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/lib/typescript/src/useChildListeners.d.ts +3 -3
- package/lib/typescript/src/useChildListeners.d.ts.map +1 -1
- package/lib/typescript/src/useDescriptors.d.ts +13 -11
- package/lib/typescript/src/useDescriptors.d.ts.map +1 -1
- package/lib/typescript/src/useEventEmitter.d.ts +2 -2
- package/lib/typescript/src/useEventEmitter.d.ts.map +1 -1
- package/lib/typescript/src/useFocusEffect.d.ts.map +1 -1
- package/lib/typescript/src/useFocusEvents.d.ts +2 -2
- package/lib/typescript/src/useFocusEvents.d.ts.map +1 -1
- package/lib/typescript/src/useFocusedListenersChildrenAdapter.d.ts +2 -2
- package/lib/typescript/src/useFocusedListenersChildrenAdapter.d.ts.map +1 -1
- package/lib/typescript/src/useIsFocused.d.ts +4 -0
- package/lib/typescript/src/useIsFocused.d.ts.map +1 -1
- package/lib/typescript/src/useKeyedChildListeners.d.ts +4 -3
- package/lib/typescript/src/useKeyedChildListeners.d.ts.map +1 -1
- package/lib/typescript/src/useLazyValue.d.ts.map +1 -1
- package/lib/typescript/src/useNavigation.d.ts +4 -2
- package/lib/typescript/src/useNavigation.d.ts.map +1 -1
- package/lib/typescript/src/useNavigationBuilder.d.ts +22 -63
- package/lib/typescript/src/useNavigationBuilder.d.ts.map +1 -1
- package/lib/typescript/src/useNavigationCache.d.ts +6 -5
- package/lib/typescript/src/useNavigationCache.d.ts.map +1 -1
- package/lib/typescript/src/useNavigationContainerRef.d.ts +1 -1
- package/lib/typescript/src/useNavigationContainerRef.d.ts.map +1 -1
- package/lib/typescript/src/useNavigationHelpers.d.ts +18 -58
- package/lib/typescript/src/useNavigationHelpers.d.ts.map +1 -1
- package/lib/typescript/src/useNavigationState.d.ts +19 -3
- package/lib/typescript/src/useNavigationState.d.ts.map +1 -1
- package/lib/typescript/src/useOnAction.d.ts +9 -9
- package/lib/typescript/src/useOnAction.d.ts.map +1 -1
- package/lib/typescript/src/useOnGetNavigation.d.ts +12 -0
- package/lib/typescript/src/useOnGetNavigation.d.ts.map +1 -0
- package/lib/typescript/src/useOnGetState.d.ts +1 -1
- package/lib/typescript/src/useOnGetState.d.ts.map +1 -1
- package/lib/typescript/src/useOnPreventRemove.d.ts +5 -9
- package/lib/typescript/src/useOnPreventRemove.d.ts.map +1 -1
- package/lib/typescript/src/useOnRouteFocus.d.ts +6 -6
- package/lib/typescript/src/useOnRouteFocus.d.ts.map +1 -1
- package/lib/typescript/src/useOptionsGetters.d.ts +4 -4
- package/lib/typescript/src/useOptionsGetters.d.ts.map +1 -1
- package/lib/typescript/src/usePreventRemove.d.ts.map +1 -1
- package/lib/typescript/src/usePreventRemoveContext.d.ts +2 -1
- package/lib/typescript/src/usePreventRemoveContext.d.ts.map +1 -1
- package/lib/typescript/src/useRegisterNavigator.d.ts.map +1 -1
- package/lib/typescript/src/useRoute.d.ts +7 -3
- package/lib/typescript/src/useRoute.d.ts.map +1 -1
- package/lib/typescript/src/useRouteCache.d.ts +7 -12
- package/lib/typescript/src/useRouteCache.d.ts.map +1 -1
- package/lib/typescript/src/useScheduleUpdate.d.ts +5 -4
- package/lib/typescript/src/useScheduleUpdate.d.ts.map +1 -1
- package/lib/typescript/src/useStateForPath.d.ts +1 -1
- package/lib/typescript/src/useStateForPath.d.ts.map +1 -1
- package/lib/typescript/src/useSyncState.d.ts +1 -0
- package/lib/typescript/src/useSyncState.d.ts.map +1 -1
- package/lib/typescript/src/utilities.d.ts +79 -39
- package/lib/typescript/src/utilities.d.ts.map +1 -1
- package/lib/typescript/src/validatePathConfig.d.ts.map +1 -1
- package/package.json +26 -22
- package/src/BaseNavigationContainer.tsx +459 -332
- package/src/ConsumedParamsContext.tsx +5 -0
- package/src/DataLoading.tsx +206 -0
- package/src/NavigationBuilderContext.tsx +44 -25
- package/src/NavigationFocusedRouteStateContext.tsx +4 -4
- package/src/NavigationIndependentTree.tsx +60 -12
- package/src/NavigationProvider.tsx +59 -21
- package/src/NavigationRootContext.tsx +11 -0
- package/src/NavigationStateContext.tsx +6 -6
- package/src/PreventRemoveContext.tsx +1 -0
- package/src/PreventRemoveProvider.tsx +55 -56
- package/src/SceneView.tsx +31 -57
- package/src/StaticNavigation.tsx +715 -328
- package/src/StaticTreeContext.tsx +7 -0
- package/src/createNavigationContainerRef.tsx +12 -11
- package/src/createNavigatorFactory.tsx +92 -8
- package/src/findFocusedRoute.tsx +16 -16
- package/src/getActionFromState.tsx +87 -51
- package/src/getPathFromState.tsx +310 -134
- package/src/getPatternParts.tsx +98 -16
- package/src/getStateFromPath.tsx +971 -344
- package/src/getStateFromRouteParams.tsx +59 -0
- package/src/index.tsx +57 -6
- package/src/internal.tsx +2 -0
- package/src/isRecordEqual.tsx +3 -1
- package/src/queryString.ts +109 -0
- package/src/theming/useTheme.tsx +1 -1
- package/src/types.tsx +799 -377
- package/src/useDescriptors.tsx +151 -43
- package/src/useEventEmitter.tsx +53 -39
- package/src/useFocusEffect.tsx +5 -2
- package/src/useFocusEvents.tsx +19 -8
- package/src/useFocusedListenersChildrenAdapter.tsx +3 -3
- package/src/useIsFocused.tsx +18 -21
- package/src/useKeyedChildListeners.tsx +5 -2
- package/src/useLazyValue.tsx +2 -2
- package/src/useNavigation.tsx +9 -26
- package/src/useNavigationBuilder.tsx +370 -201
- package/src/useNavigationCache.tsx +148 -123
- package/src/useNavigationHelpers.tsx +27 -37
- package/src/useNavigationIndependentTree.tsx +1 -1
- package/src/useNavigationState.tsx +207 -69
- package/src/useOnAction.tsx +92 -18
- package/src/useOnGetNavigation.tsx +30 -0
- package/src/useOnGetState.tsx +8 -4
- package/src/useOnPreventRemove.tsx +78 -43
- package/src/useOnRouteFocus.tsx +11 -15
- package/src/useOptionsGetters.tsx +44 -30
- package/src/usePreventRemove.tsx +13 -5
- package/src/usePreventRemoveContext.tsx +2 -2
- package/src/useRegisterNavigator.tsx +4 -4
- package/src/useRoute.tsx +20 -6
- package/src/useRouteCache.tsx +57 -35
- package/src/useScheduleUpdate.tsx +13 -11
- package/src/useStateForPath.tsx +1 -1
- package/src/useSyncState.tsx +39 -15
- package/src/utilities.tsx +151 -35
- package/src/validatePathConfig.tsx +86 -57
- package/lib/module/CurrentRenderContext.js +0 -10
- package/lib/module/CurrentRenderContext.js.map +0 -1
- package/lib/module/NavigationMetaContext.js +0 -15
- package/lib/module/NavigationMetaContext.js.map +0 -1
- package/lib/module/checkSerializable.js +0 -46
- package/lib/module/checkSerializable.js.map +0 -1
- package/lib/module/useComponent.js +0 -34
- package/lib/module/useComponent.js.map +0 -1
- package/lib/module/useCurrentRender.js +0 -19
- package/lib/module/useCurrentRender.js.map +0 -1
- package/lib/typescript/src/CurrentRenderContext.d.ts +0 -9
- package/lib/typescript/src/CurrentRenderContext.d.ts.map +0 -1
- package/lib/typescript/src/NavigationMetaContext.d.ts +0 -12
- package/lib/typescript/src/NavigationMetaContext.d.ts.map +0 -1
- package/lib/typescript/src/checkSerializable.d.ts +0 -10
- package/lib/typescript/src/checkSerializable.d.ts.map +0 -1
- package/lib/typescript/src/useComponent.d.ts +0 -7
- package/lib/typescript/src/useComponent.d.ts.map +0 -1
- package/lib/typescript/src/useCurrentRender.d.ts +0 -14
- package/lib/typescript/src/useCurrentRender.d.ts.map +0 -1
- package/src/CurrentRenderContext.tsx +0 -9
- package/src/NavigationMetaContext.tsx +0 -14
- package/src/checkSerializable.tsx +0 -74
- package/src/useComponent.tsx +0 -37
- package/src/useCurrentRender.tsx +0 -31
package/src/getStateFromPath.tsx
CHANGED
|
@@ -1,33 +1,52 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
InitialState,
|
|
3
|
-
NavigationState,
|
|
4
|
-
PartialState,
|
|
5
|
-
} from '@react-navigation/routers';
|
|
6
|
-
import escape from 'escape-string-regexp';
|
|
7
|
-
import * as queryString from 'query-string';
|
|
1
|
+
import type { NavigationState, PartialState } from '@react-navigation/routers';
|
|
8
2
|
|
|
9
3
|
import { arrayStartsWith } from './arrayStartsWith';
|
|
10
|
-
import {
|
|
11
|
-
|
|
4
|
+
import {
|
|
5
|
+
combinePatternParts,
|
|
6
|
+
getPatternParts,
|
|
7
|
+
type PatternPartRepeat,
|
|
8
|
+
} from './getPatternParts';
|
|
12
9
|
import { isArrayEqual } from './isArrayEqual';
|
|
10
|
+
import * as queryString from './queryString';
|
|
13
11
|
import type { PathConfig, PathConfigMap } from './types';
|
|
12
|
+
import type {
|
|
13
|
+
StandardSchemaV1,
|
|
14
|
+
StandardSchemaValidationResult,
|
|
15
|
+
} from './utilities';
|
|
14
16
|
import { validatePathConfig } from './validatePathConfig';
|
|
15
17
|
|
|
16
18
|
type Options<ParamList extends {}> = {
|
|
17
|
-
path?: string;
|
|
18
|
-
initialRouteName?: string;
|
|
19
|
+
path?: string | undefined;
|
|
20
|
+
initialRouteName?: string | undefined;
|
|
19
21
|
screens: PathConfigMap<ParamList>;
|
|
20
22
|
};
|
|
21
23
|
|
|
22
|
-
type
|
|
24
|
+
type ParseConfigValue =
|
|
25
|
+
| ((value: string) => unknown)
|
|
26
|
+
| StandardSchemaV1<unknown, unknown>;
|
|
27
|
+
|
|
28
|
+
type ParseConfig = Record<string, ParseConfigValue | undefined>;
|
|
23
29
|
|
|
24
30
|
type RouteConfig = {
|
|
25
31
|
screen: string;
|
|
26
|
-
|
|
32
|
+
order: number;
|
|
33
|
+
regex?: RegExp | undefined;
|
|
34
|
+
pattern: string;
|
|
27
35
|
segments: string[];
|
|
28
|
-
|
|
36
|
+
segmentRanks: number[];
|
|
37
|
+
params: {
|
|
38
|
+
screen: string;
|
|
39
|
+
name: string;
|
|
40
|
+
index: number;
|
|
41
|
+
regex?: RegExp | undefined;
|
|
42
|
+
repeat?: PatternPartRepeat;
|
|
43
|
+
}[];
|
|
29
44
|
routeNames: string[];
|
|
30
|
-
parse?: ParseConfig;
|
|
45
|
+
parse?: ParseConfig | undefined;
|
|
46
|
+
explicitParamNames?: Set<string> | undefined;
|
|
47
|
+
pathParamNames: Set<string>;
|
|
48
|
+
hasNestedScreens: boolean;
|
|
49
|
+
shared: boolean;
|
|
31
50
|
};
|
|
32
51
|
|
|
33
52
|
type InitialRouteConfig = {
|
|
@@ -35,19 +54,144 @@ type InitialRouteConfig = {
|
|
|
35
54
|
parentScreens: string[];
|
|
36
55
|
};
|
|
37
56
|
|
|
38
|
-
type ResultState = PartialState<NavigationState> & {
|
|
39
|
-
state?: ResultState;
|
|
40
|
-
};
|
|
41
|
-
|
|
42
57
|
type ParsedRoute = {
|
|
43
58
|
name: string;
|
|
44
|
-
path?: string;
|
|
45
59
|
params?: Record<string, unknown> | undefined;
|
|
60
|
+
path?: string | undefined;
|
|
46
61
|
};
|
|
47
62
|
|
|
63
|
+
type RoutePatternPart =
|
|
64
|
+
| {
|
|
65
|
+
type: 'static';
|
|
66
|
+
segment: string;
|
|
67
|
+
rank: number;
|
|
68
|
+
pattern: string;
|
|
69
|
+
}
|
|
70
|
+
| {
|
|
71
|
+
type: 'param';
|
|
72
|
+
screen: string;
|
|
73
|
+
segment: string;
|
|
74
|
+
rank: number;
|
|
75
|
+
name: string;
|
|
76
|
+
pattern?: string;
|
|
77
|
+
regex?: RegExp;
|
|
78
|
+
optional?: boolean;
|
|
79
|
+
repeat?: PatternPartRepeat;
|
|
80
|
+
}
|
|
81
|
+
| {
|
|
82
|
+
type: 'wildcard';
|
|
83
|
+
segment: '*';
|
|
84
|
+
rank: number;
|
|
85
|
+
};
|
|
86
|
+
|
|
48
87
|
type ConfigResources = {
|
|
49
88
|
initialRoutes: InitialRouteConfig[];
|
|
50
89
|
configs: RouteConfig[];
|
|
90
|
+
configsWithZeroOrMore: RouteConfig[];
|
|
91
|
+
configsByScreen: Record<string, RouteConfig[]>;
|
|
92
|
+
configsByPattern: Record<string, RouteConfig[]>;
|
|
93
|
+
prefixRegex?: RegExp | undefined;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const INVALID_SCHEMA_RESULT_ERROR =
|
|
97
|
+
'Invalid validation result from schema. It should be an object with either "value" or "issues" property and cannot be asynchronous.';
|
|
98
|
+
|
|
99
|
+
const INVALID_PARSER_ERROR =
|
|
100
|
+
'Invalid parser. Expected a function or a Standard Schema V1 object.';
|
|
101
|
+
|
|
102
|
+
const PARAM_GROUP_PREFIX = 'param_';
|
|
103
|
+
|
|
104
|
+
const NESTED_SCREEN_PARAM_NAMES = [
|
|
105
|
+
'screen',
|
|
106
|
+
'params',
|
|
107
|
+
'initial',
|
|
108
|
+
'path',
|
|
109
|
+
'merge',
|
|
110
|
+
'pop',
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
const REGEX_SYNTAX_CHAR = /[|\\{}()[\]^$+*?.]/;
|
|
114
|
+
const HEX_DIGITS = '0123456789ABCDEF';
|
|
115
|
+
|
|
116
|
+
const getStaticSegmentPattern = (segment: string) => {
|
|
117
|
+
let pattern = '';
|
|
118
|
+
|
|
119
|
+
for (const char of segment) {
|
|
120
|
+
const code = char.charCodeAt(0);
|
|
121
|
+
|
|
122
|
+
let escaped = char;
|
|
123
|
+
|
|
124
|
+
// A hyphen uses a hexadecimal escape because a regular backslash escape
|
|
125
|
+
// isn't valid outside character classes when a regex uses Unicode mode.
|
|
126
|
+
if (char === '-') {
|
|
127
|
+
escaped = '\\x2d';
|
|
128
|
+
} else {
|
|
129
|
+
// Other characters with special meaning in a regex can use a regular
|
|
130
|
+
// backslash escape so they are matched as literal characters.
|
|
131
|
+
if (REGEX_SYNTAX_CHAR.test(char)) {
|
|
132
|
+
escaped = `\\${char}`;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ASCII characters have a single-byte percent-encoded form that can be
|
|
137
|
+
// built directly. Other characters need encodeURIComponent to encode all
|
|
138
|
+
// bytes in their UTF-8 representation.
|
|
139
|
+
const percentEncoded =
|
|
140
|
+
char.length === 1 && code < 128
|
|
141
|
+
? `%${HEX_DIGITS.charAt(code >> 4)}${HEX_DIGITS.charAt(code & 15)}`
|
|
142
|
+
: encodeURIComponent(char);
|
|
143
|
+
|
|
144
|
+
pattern += `(?:${escaped}|${percentEncoded})`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return pattern;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const getExplicitParamNames = (parse?: ParseConfig) => {
|
|
151
|
+
const names: string[] = [];
|
|
152
|
+
|
|
153
|
+
if (parse) {
|
|
154
|
+
for (const name in parse) {
|
|
155
|
+
if (parse[name] != null) {
|
|
156
|
+
names.push(name);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return names.length ? new Set(names) : undefined;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const getStandardSchema = (parser: ParseConfigValue) => {
|
|
165
|
+
if (
|
|
166
|
+
'~standard' in parser &&
|
|
167
|
+
typeof parser['~standard'] === 'object' &&
|
|
168
|
+
parser['~standard'] !== null &&
|
|
169
|
+
'version' in parser['~standard'] &&
|
|
170
|
+
parser['~standard'].version === 1 &&
|
|
171
|
+
'validate' in parser['~standard'] &&
|
|
172
|
+
typeof parser['~standard'].validate === 'function'
|
|
173
|
+
) {
|
|
174
|
+
return parser['~standard'];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return undefined;
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const getValidationResult = (
|
|
181
|
+
schema: StandardSchemaV1<unknown, unknown>['~standard'],
|
|
182
|
+
value: unknown
|
|
183
|
+
): StandardSchemaValidationResult<unknown> => {
|
|
184
|
+
const result = schema.validate(value);
|
|
185
|
+
|
|
186
|
+
if (
|
|
187
|
+
result != null &&
|
|
188
|
+
typeof result === 'object' &&
|
|
189
|
+
('value' in result || ('issues' in result && Array.isArray(result.issues)))
|
|
190
|
+
) {
|
|
191
|
+
return result;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
throw new Error(INVALID_SCHEMA_RESULT_ERROR);
|
|
51
195
|
};
|
|
52
196
|
|
|
53
197
|
/**
|
|
@@ -73,44 +217,68 @@ type ConfigResources = {
|
|
|
73
217
|
*/
|
|
74
218
|
export function getStateFromPath<ParamList extends {}>(
|
|
75
219
|
path: string,
|
|
76
|
-
options?: Options<ParamList
|
|
77
|
-
|
|
78
|
-
|
|
220
|
+
options?: Options<ParamList>,
|
|
221
|
+
previous?: NavigationState
|
|
222
|
+
): PartialState<NavigationState> | undefined {
|
|
223
|
+
const {
|
|
224
|
+
initialRoutes,
|
|
225
|
+
configs,
|
|
226
|
+
configsWithZeroOrMore,
|
|
227
|
+
configsByScreen,
|
|
228
|
+
configsByPattern,
|
|
229
|
+
prefixRegex,
|
|
230
|
+
} = getConfigResources(options);
|
|
79
231
|
|
|
80
232
|
const screens = options?.screens;
|
|
81
233
|
|
|
234
|
+
// When a matched config is shared, the same pattern can resolve to multiple branches
|
|
235
|
+
// pick the one that best matches the current navigation state
|
|
236
|
+
const resolveSharedConfig = (config: RouteConfig) => {
|
|
237
|
+
if (config.shared) {
|
|
238
|
+
const candidates = configsByPattern[config.pattern];
|
|
239
|
+
|
|
240
|
+
if (candidates) {
|
|
241
|
+
return selectSharedConfig(config, candidates, previous, initialRoutes);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return config;
|
|
246
|
+
};
|
|
247
|
+
|
|
82
248
|
let remaining = path
|
|
83
249
|
.replace(/\/+/g, '/') // Replace multiple slash (//) with single ones
|
|
84
250
|
.replace(/^\//, '') // Remove extra leading slash
|
|
85
|
-
.replace(/\?.*$/, '')
|
|
251
|
+
.replace(/\?.*$/, '') // Remove query params which we will handle later
|
|
252
|
+
.replace(/%[0-9a-f]{2}/gi, (m) => m.toUpperCase()); // Normalize percent-encoding as hex digits are case-insensitive
|
|
86
253
|
|
|
87
254
|
// Make sure there is a trailing slash
|
|
88
255
|
remaining = remaining.endsWith('/') ? remaining : `${remaining}/`;
|
|
89
256
|
|
|
90
|
-
|
|
257
|
+
if (prefixRegex) {
|
|
258
|
+
const prefixMatch = remaining.match(prefixRegex);
|
|
91
259
|
|
|
92
|
-
|
|
93
|
-
// Make sure there is a trailing slash
|
|
94
|
-
const normalizedPrefix = prefix.endsWith('/') ? prefix : `${prefix}/`;
|
|
95
|
-
|
|
96
|
-
// If the path doesn't start with the prefix, it's not a match
|
|
97
|
-
if (!remaining.startsWith(normalizedPrefix)) {
|
|
260
|
+
if (prefixMatch == null) {
|
|
98
261
|
return undefined;
|
|
99
262
|
}
|
|
100
263
|
|
|
101
|
-
|
|
102
|
-
remaining = remaining.replace(normalizedPrefix, '');
|
|
264
|
+
remaining = remaining.slice(prefixMatch[0].length);
|
|
103
265
|
}
|
|
104
266
|
|
|
105
267
|
if (screens === undefined) {
|
|
106
268
|
// When no config is specified, use the path segments as route names
|
|
107
|
-
const routes =
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
269
|
+
const routes: ParsedRoute[] = [];
|
|
270
|
+
|
|
271
|
+
for (const segment of remaining.split('/')) {
|
|
272
|
+
if (!segment) {
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
routes.push({ name: decodeURIComponent(segment) });
|
|
278
|
+
} catch {
|
|
279
|
+
return undefined;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
114
282
|
|
|
115
283
|
if (routes.length) {
|
|
116
284
|
return createNestedStateObject(path, routes, initialRoutes);
|
|
@@ -122,41 +290,143 @@ export function getStateFromPath<ParamList extends {}>(
|
|
|
122
290
|
if (remaining === '/') {
|
|
123
291
|
// We need to add special handling of empty path so navigation to empty path also works
|
|
124
292
|
// When handling empty path, we should only look at the root level config
|
|
125
|
-
const match = configs.find((config) => config.
|
|
293
|
+
const match = configs.find((config) => config.pattern === '');
|
|
126
294
|
|
|
127
295
|
if (match) {
|
|
128
|
-
|
|
296
|
+
const config = resolveSharedConfig(match);
|
|
297
|
+
|
|
298
|
+
const state = createNestedStateObject(
|
|
129
299
|
path,
|
|
130
|
-
|
|
300
|
+
config.routeNames.map((name) => ({ name })),
|
|
131
301
|
initialRoutes,
|
|
132
|
-
|
|
302
|
+
config
|
|
133
303
|
);
|
|
304
|
+
|
|
305
|
+
if (state) {
|
|
306
|
+
return state;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
for (const config of configsWithZeroOrMore) {
|
|
311
|
+
const routes = matchAgainstConfig('', config, configsByScreen);
|
|
312
|
+
|
|
313
|
+
if (routes) {
|
|
314
|
+
let selectedConfig = resolveSharedConfig(config);
|
|
315
|
+
let selectedRoutes = routes;
|
|
316
|
+
|
|
317
|
+
if (selectedConfig !== config) {
|
|
318
|
+
const rematchedRoutes = matchAgainstConfig(
|
|
319
|
+
'',
|
|
320
|
+
selectedConfig,
|
|
321
|
+
configsByScreen
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
if (rematchedRoutes === undefined) {
|
|
325
|
+
selectedConfig = config;
|
|
326
|
+
} else {
|
|
327
|
+
selectedRoutes = rematchedRoutes;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const state = createNestedStateObject(
|
|
332
|
+
path,
|
|
333
|
+
selectedRoutes,
|
|
334
|
+
initialRoutes,
|
|
335
|
+
selectedConfig
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
if (state) {
|
|
339
|
+
return state;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
134
342
|
}
|
|
135
343
|
|
|
136
344
|
return undefined;
|
|
137
345
|
}
|
|
138
346
|
|
|
139
|
-
let result: PartialState<NavigationState> | undefined;
|
|
140
|
-
let current: PartialState<NavigationState> | undefined;
|
|
141
|
-
|
|
142
347
|
// We match the whole path against the regex instead of segments
|
|
143
348
|
// This makes sure matches such as wildcard will catch any unmatched routes, even if nested
|
|
144
|
-
const
|
|
349
|
+
const firstRawSegment = remaining.split('/')[0];
|
|
350
|
+
|
|
351
|
+
let firstDecodedSegment: string | undefined;
|
|
145
352
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
353
|
+
try {
|
|
354
|
+
firstDecodedSegment =
|
|
355
|
+
firstRawSegment != null ? decodeURIComponent(firstRawSegment) : undefined;
|
|
356
|
+
} catch {
|
|
357
|
+
firstDecodedSegment = undefined;
|
|
151
358
|
}
|
|
152
359
|
|
|
153
|
-
|
|
154
|
-
|
|
360
|
+
for (const config of configs) {
|
|
361
|
+
// Regex matching is expensive, so skip configs that can't match this path.
|
|
362
|
+
if (
|
|
363
|
+
!canMatchFirstSegment(
|
|
364
|
+
config.segments[0],
|
|
365
|
+
firstDecodedSegment,
|
|
366
|
+
firstRawSegment
|
|
367
|
+
)
|
|
368
|
+
) {
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const routes = matchAgainstConfig(remaining, config, configsByScreen);
|
|
373
|
+
|
|
374
|
+
if (routes === undefined) {
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
let selectedConfig = resolveSharedConfig(config);
|
|
379
|
+
let selectedRoutes = routes;
|
|
380
|
+
|
|
381
|
+
if (selectedConfig !== config) {
|
|
382
|
+
const rematchedRoutes = matchAgainstConfig(
|
|
383
|
+
remaining,
|
|
384
|
+
selectedConfig,
|
|
385
|
+
configsByScreen
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
if (rematchedRoutes === undefined) {
|
|
389
|
+
selectedConfig = config;
|
|
390
|
+
} else {
|
|
391
|
+
selectedRoutes = rematchedRoutes;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const state = createNestedStateObject(
|
|
396
|
+
path,
|
|
397
|
+
selectedRoutes,
|
|
398
|
+
initialRoutes,
|
|
399
|
+
selectedConfig
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
if (state !== undefined) {
|
|
403
|
+
return state;
|
|
404
|
+
}
|
|
155
405
|
}
|
|
156
406
|
|
|
157
|
-
return
|
|
407
|
+
return undefined;
|
|
158
408
|
}
|
|
159
409
|
|
|
410
|
+
const canMatchFirstSegment = (
|
|
411
|
+
configSegment: string | undefined,
|
|
412
|
+
decodedSegment: string | undefined,
|
|
413
|
+
rawSegment: string | undefined
|
|
414
|
+
) => {
|
|
415
|
+
if (decodedSegment === undefined) {
|
|
416
|
+
return true;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (configSegment === undefined) {
|
|
420
|
+
return false;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (configSegment === '*' || configSegment.startsWith(':')) {
|
|
424
|
+
return true;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return configSegment === decodedSegment || configSegment === rawSegment;
|
|
428
|
+
};
|
|
429
|
+
|
|
160
430
|
/**
|
|
161
431
|
* Reference to the last used config resources. This is used to avoid recomputing the config resources when the options are the same.
|
|
162
432
|
*/
|
|
@@ -165,11 +435,17 @@ const cachedConfigResources = new WeakMap<Options<{}>, ConfigResources>();
|
|
|
165
435
|
function getConfigResources<ParamList extends {}>(
|
|
166
436
|
options: Options<ParamList> | undefined
|
|
167
437
|
) {
|
|
168
|
-
if (!options)
|
|
438
|
+
if (!options) {
|
|
439
|
+
return prepareConfigResources();
|
|
440
|
+
}
|
|
169
441
|
|
|
170
442
|
const cached = cachedConfigResources.get(options);
|
|
171
443
|
|
|
172
|
-
if (cached)
|
|
444
|
+
if (cached) {
|
|
445
|
+
return cached;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
validatePathConfig(options);
|
|
173
449
|
|
|
174
450
|
const resources = prepareConfigResources(options);
|
|
175
451
|
|
|
@@ -179,21 +455,50 @@ function getConfigResources<ParamList extends {}>(
|
|
|
179
455
|
}
|
|
180
456
|
|
|
181
457
|
function prepareConfigResources(options?: Options<{}>) {
|
|
182
|
-
if (options) {
|
|
183
|
-
validatePathConfig(options);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
458
|
const initialRoutes = getInitialRoutes(options);
|
|
187
459
|
const configs = getSortedNormalizedConfigs(initialRoutes, options?.screens);
|
|
460
|
+
const prefix = options?.path?.replace(/^\//, '');
|
|
188
461
|
|
|
189
|
-
|
|
462
|
+
let prefixRegex: RegExp | undefined;
|
|
190
463
|
|
|
191
|
-
|
|
464
|
+
if (prefix) {
|
|
465
|
+
const normalizedPrefix = prefix.endsWith('/') ? prefix : `${prefix}/`;
|
|
466
|
+
const prefixPattern = normalizedPrefix
|
|
467
|
+
.split('/')
|
|
468
|
+
.map(getStaticSegmentPattern)
|
|
469
|
+
.join('/');
|
|
470
|
+
|
|
471
|
+
prefixRegex = new RegExp(`^${prefixPattern}`);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const configsByScreen: Record<string, RouteConfig[]> = {};
|
|
475
|
+
const configsByPattern: Record<string, RouteConfig[]> = {};
|
|
476
|
+
const configsWithZeroOrMore: RouteConfig[] = [];
|
|
477
|
+
|
|
478
|
+
for (const c of configs) {
|
|
479
|
+
if (c.params.some((param) => param.repeat === 'zero-or-more')) {
|
|
480
|
+
configsWithZeroOrMore.push(c);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
(configsByScreen[c.screen] ??= []).push(c);
|
|
484
|
+
|
|
485
|
+
const existing = configsByPattern[c.pattern];
|
|
486
|
+
|
|
487
|
+
if (existing) {
|
|
488
|
+
checkForDuplicatedConfigs(existing[existing.length - 1], c);
|
|
489
|
+
existing.push(c);
|
|
490
|
+
} else {
|
|
491
|
+
configsByPattern[c.pattern] = [c];
|
|
492
|
+
}
|
|
493
|
+
}
|
|
192
494
|
|
|
193
495
|
return {
|
|
194
496
|
initialRoutes,
|
|
195
497
|
configs,
|
|
196
|
-
|
|
498
|
+
configsWithZeroOrMore,
|
|
499
|
+
configsByScreen,
|
|
500
|
+
configsByPattern,
|
|
501
|
+
prefixRegex,
|
|
197
502
|
};
|
|
198
503
|
}
|
|
199
504
|
|
|
@@ -215,216 +520,304 @@ function getSortedNormalizedConfigs(
|
|
|
215
520
|
screens: Record<string, string | PathConfig<{}>> = {}
|
|
216
521
|
) {
|
|
217
522
|
// Create a normalized configs array which will be easier to use
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
// If 2 patterns are same, move the one with less route names up
|
|
233
|
-
// This is an error state, so it's only useful for consistent error messages
|
|
234
|
-
if (isArrayEqual(a.segments, b.segments)) {
|
|
235
|
-
return b.routeNames.join('>').localeCompare(a.routeNames.join('>'));
|
|
236
|
-
}
|
|
523
|
+
const configs: RouteConfig[] = [];
|
|
524
|
+
|
|
525
|
+
for (const key in screens) {
|
|
526
|
+
setupNormalizedConfigs(key, screens, initialRoutes, configs, [], [], []);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
for (let order = 0; order < configs.length; order++) {
|
|
530
|
+
const config = configs[order];
|
|
531
|
+
|
|
532
|
+
if (config) {
|
|
533
|
+
config.order = order;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
237
536
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
537
|
+
return configs.sort((a, b) => {
|
|
538
|
+
// Sort config from most specific to least specific:
|
|
539
|
+
// - static segments
|
|
540
|
+
// - params with regex
|
|
541
|
+
// - regular params
|
|
542
|
+
// - one-or-more params
|
|
543
|
+
// - the end of the pattern
|
|
544
|
+
// - zero-or-more params
|
|
545
|
+
// - wildcard
|
|
546
|
+
|
|
547
|
+
// If 2 patterns are same, move the one with less route names up
|
|
548
|
+
// This is an error state, so it's only useful for consistent error messages
|
|
549
|
+
if (a.pattern === b.pattern && isArrayEqual(a.segments, b.segments)) {
|
|
550
|
+
if (
|
|
551
|
+
a.routeNames.length > b.routeNames.length &&
|
|
552
|
+
arrayStartsWith(a.routeNames, b.routeNames)
|
|
553
|
+
) {
|
|
241
554
|
return -1;
|
|
242
555
|
}
|
|
243
556
|
|
|
244
|
-
if (
|
|
557
|
+
if (
|
|
558
|
+
b.routeNames.length > a.routeNames.length &&
|
|
559
|
+
arrayStartsWith(b.routeNames, a.routeNames)
|
|
560
|
+
) {
|
|
245
561
|
return 1;
|
|
246
562
|
}
|
|
247
563
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
if (a.segments[i] == null) {
|
|
251
|
-
return 1;
|
|
252
|
-
}
|
|
564
|
+
return a.routeNames.length - b.routeNames.length || a.order - b.order;
|
|
565
|
+
}
|
|
253
566
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
567
|
+
for (let i = 0; i < Math.max(a.segments.length, b.segments.length); i++) {
|
|
568
|
+
const rank = (a.segmentRanks[i] ?? 5) - (b.segmentRanks[i] ?? 5);
|
|
569
|
+
|
|
570
|
+
if (rank !== 0) {
|
|
571
|
+
return rank;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
return a.order - b.order;
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const getSegmentRank = (segment: string | undefined) => {
|
|
580
|
+
if (segment === undefined) {
|
|
581
|
+
return 5;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
if (segment === '*') {
|
|
585
|
+
return 8;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
if (!segment.startsWith(':')) {
|
|
589
|
+
return 0;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const regex = segment.includes('(');
|
|
593
|
+
|
|
594
|
+
if (segment.endsWith('+')) {
|
|
595
|
+
return regex ? 3 : 4;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
if (segment.endsWith('*')) {
|
|
599
|
+
return regex ? 6 : 7;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
return regex ? 1 : 2;
|
|
603
|
+
};
|
|
604
|
+
|
|
605
|
+
// Throw if two configs resolve to the same pattern but conflicting screens
|
|
606
|
+
function checkForDuplicatedConfigs(
|
|
607
|
+
existing: RouteConfig | undefined,
|
|
608
|
+
config: RouteConfig
|
|
609
|
+
) {
|
|
610
|
+
if (!existing) {
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const a = existing.routeNames;
|
|
615
|
+
const b = config.routeNames;
|
|
616
|
+
|
|
617
|
+
// It's not a problem if the path string omitted from an innermost screen
|
|
618
|
+
// For example, it's ok if a path resolves to `A > B > C` or `A > B`
|
|
619
|
+
const intersects =
|
|
620
|
+
a.length > b.length ? arrayStartsWith(a, b) : arrayStartsWith(b, a);
|
|
621
|
+
|
|
622
|
+
if (!intersects && (!existing.shared || !config.shared)) {
|
|
623
|
+
throw new Error(
|
|
624
|
+
`Found conflicting screens with the same pattern. The pattern '${
|
|
625
|
+
config.pattern
|
|
626
|
+
}' resolves to both '${a.join(' > ')}' and '${b.join(
|
|
627
|
+
' > '
|
|
628
|
+
)}'. Patterns must be unique and cannot resolve to more than one screen unless shared: true is specified.`
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
const matchAgainstConfig = (
|
|
634
|
+
remaining: string,
|
|
635
|
+
config: RouteConfig,
|
|
636
|
+
configsByScreen: Record<string, RouteConfig[]>
|
|
637
|
+
) => {
|
|
638
|
+
if (!config.regex) {
|
|
639
|
+
return undefined;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
const match = remaining.match(config.regex);
|
|
643
|
+
|
|
644
|
+
if (!match) {
|
|
645
|
+
return undefined;
|
|
646
|
+
}
|
|
258
647
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
const aParam = a.segments[i].startsWith(':');
|
|
262
|
-
const bParam = b.segments[i].startsWith(':');
|
|
263
|
-
const aRegex = aParam && a.segments[i].includes('(');
|
|
264
|
-
const bRegex = bParam && b.segments[i].includes('(');
|
|
648
|
+
let validationFailed = false;
|
|
649
|
+
const matchedRoutes: ParsedRoute[] = [];
|
|
265
650
|
|
|
266
|
-
|
|
267
|
-
|
|
651
|
+
for (const [index, routeName] of config.routeNames.entries()) {
|
|
652
|
+
// Check matching name, route names and pattern in case same screen is used
|
|
653
|
+
// at different levels or branches in config
|
|
654
|
+
const routeConfig = configsByScreen[routeName]?.find(
|
|
655
|
+
(c) =>
|
|
656
|
+
index === c.routeNames.length - 1 &&
|
|
657
|
+
arrayStartsWith(config.routeNames, c.routeNames) &&
|
|
658
|
+
arrayStartsWith(config.segments, c.segments)
|
|
659
|
+
);
|
|
660
|
+
|
|
661
|
+
let params: Record<string, unknown> | undefined;
|
|
662
|
+
let hasParams = false;
|
|
663
|
+
|
|
664
|
+
if (routeConfig && match.groups) {
|
|
665
|
+
params = {};
|
|
666
|
+
|
|
667
|
+
for (const param of routeConfig.params) {
|
|
668
|
+
if (param.screen !== routeName) {
|
|
268
669
|
continue;
|
|
269
670
|
}
|
|
270
671
|
|
|
271
|
-
|
|
272
|
-
if (aWildCard && !bWildCard) {
|
|
273
|
-
return 1;
|
|
274
|
-
}
|
|
672
|
+
let value = match.groups[`${PARAM_GROUP_PREFIX}${param.index}`];
|
|
275
673
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
return -1;
|
|
674
|
+
if (param.repeat && value != null) {
|
|
675
|
+
value = value.replace(/\/$/, '');
|
|
279
676
|
}
|
|
280
677
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
return 1;
|
|
284
|
-
}
|
|
678
|
+
let decoded: string;
|
|
679
|
+
let decodedSegments: string[] | undefined;
|
|
285
680
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
681
|
+
try {
|
|
682
|
+
if (param.repeat) {
|
|
683
|
+
if (value == null) {
|
|
684
|
+
decodedSegments = [];
|
|
685
|
+
} else {
|
|
686
|
+
decodedSegments = value.split('/').map(decodeURIComponent);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
decoded = decodedSegments.join('/');
|
|
690
|
+
} else {
|
|
691
|
+
if (value == null) {
|
|
692
|
+
params[param.name] = undefined;
|
|
693
|
+
hasParams = true;
|
|
694
|
+
continue;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
decoded = decodeURIComponent(value);
|
|
698
|
+
}
|
|
699
|
+
} catch {
|
|
700
|
+
return undefined;
|
|
289
701
|
}
|
|
290
702
|
|
|
291
|
-
//
|
|
292
|
-
|
|
293
|
-
|
|
703
|
+
// Percent-encoded values are matched permissively
|
|
704
|
+
// So validate the decoded value against the custom regex
|
|
705
|
+
if (param.regex) {
|
|
706
|
+
const regex = param.regex;
|
|
707
|
+
const values = decodedSegments ?? [decoded];
|
|
708
|
+
const rawValues = param.repeat ? (value?.split('/') ?? []) : [value];
|
|
709
|
+
|
|
710
|
+
if (
|
|
711
|
+
values.some(
|
|
712
|
+
(item, index) => item !== rawValues[index] && !regex.test(item)
|
|
713
|
+
)
|
|
714
|
+
) {
|
|
715
|
+
return undefined;
|
|
716
|
+
}
|
|
294
717
|
}
|
|
295
718
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
719
|
+
const parser = routeConfig.parse?.[param.name];
|
|
720
|
+
|
|
721
|
+
if (!parser) {
|
|
722
|
+
params[param.name] = decoded;
|
|
723
|
+
hasParams = true;
|
|
724
|
+
continue;
|
|
299
725
|
}
|
|
300
|
-
}
|
|
301
726
|
|
|
302
|
-
|
|
303
|
-
});
|
|
304
|
-
}
|
|
727
|
+
const schema = getStandardSchema(parser);
|
|
305
728
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
configs.reduce<Record<string, RouteConfig>>((acc, config) => {
|
|
309
|
-
const pattern = config.segments.join('/');
|
|
310
|
-
|
|
311
|
-
if (acc[pattern]) {
|
|
312
|
-
const a = acc[pattern].routeNames;
|
|
313
|
-
const b = config.routeNames;
|
|
314
|
-
|
|
315
|
-
// It's not a problem if the path string omitted from a inner most screen
|
|
316
|
-
// For example, it's ok if a path resolves to `A > B > C` or `A > B`
|
|
317
|
-
const intersects =
|
|
318
|
-
a.length > b.length
|
|
319
|
-
? b.every((it, i) => a[i] === it)
|
|
320
|
-
: a.every((it, i) => b[i] === it);
|
|
321
|
-
|
|
322
|
-
if (!intersects) {
|
|
323
|
-
throw new Error(
|
|
324
|
-
`Found conflicting screens with the same pattern. The pattern '${
|
|
325
|
-
pattern
|
|
326
|
-
}' resolves to both '${a.join(' > ')}' and '${b.join(
|
|
327
|
-
' > '
|
|
328
|
-
)}'. Patterns must be unique and cannot resolve to more than one screen.`
|
|
329
|
-
);
|
|
330
|
-
}
|
|
331
|
-
}
|
|
729
|
+
if (schema) {
|
|
730
|
+
const result = getValidationResult(schema, decoded);
|
|
332
731
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
}
|
|
732
|
+
if (result.issues) {
|
|
733
|
+
validationFailed = true;
|
|
734
|
+
break;
|
|
735
|
+
}
|
|
338
736
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
regex: c.regex ? new RegExp(c.regex.source + '$') : undefined,
|
|
344
|
-
}));
|
|
345
|
-
}
|
|
737
|
+
params[param.name] = result.value;
|
|
738
|
+
hasParams = true;
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
346
741
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
742
|
+
if (typeof parser === 'function') {
|
|
743
|
+
params[param.name] = parser(decoded);
|
|
744
|
+
hasParams = true;
|
|
745
|
+
continue;
|
|
746
|
+
}
|
|
350
747
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
if (!config.regex) {
|
|
354
|
-
continue;
|
|
355
|
-
}
|
|
748
|
+
throw new Error(INVALID_PARSER_ERROR);
|
|
749
|
+
}
|
|
356
750
|
|
|
357
|
-
|
|
751
|
+
if (validationFailed) {
|
|
752
|
+
// A failed param validation invalidates the whole nested route chain for this config.
|
|
753
|
+
break;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
358
756
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
c.screen === routeName &&
|
|
366
|
-
arrayStartsWith(config.segments, c.segments)
|
|
367
|
-
);
|
|
368
|
-
});
|
|
757
|
+
if (params && hasParams) {
|
|
758
|
+
matchedRoutes.push({ name: routeName, params });
|
|
759
|
+
} else {
|
|
760
|
+
matchedRoutes.push({ name: routeName });
|
|
761
|
+
}
|
|
762
|
+
}
|
|
369
763
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
Object.entries(match.groups)
|
|
374
|
-
.map(([key, value]) => {
|
|
375
|
-
const index = Number(key.replace('param_', ''));
|
|
376
|
-
const param = routeConfig.params.find(
|
|
377
|
-
(it) => it.index === index
|
|
378
|
-
);
|
|
379
|
-
|
|
380
|
-
if (param?.screen === routeName && param?.name) {
|
|
381
|
-
return [param.name, value];
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
return null;
|
|
385
|
-
})
|
|
386
|
-
.filter((it) => it != null)
|
|
387
|
-
.map(([key, value]) => {
|
|
388
|
-
if (value == null) {
|
|
389
|
-
return [key, undefined];
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
const decoded = decodeURIComponent(value);
|
|
393
|
-
const parsed = routeConfig.parse?.[key]
|
|
394
|
-
? routeConfig.parse[key](decoded)
|
|
395
|
-
: decoded;
|
|
396
|
-
|
|
397
|
-
return [key, parsed];
|
|
398
|
-
})
|
|
399
|
-
)
|
|
400
|
-
: undefined;
|
|
764
|
+
if (validationFailed) {
|
|
765
|
+
return undefined;
|
|
766
|
+
}
|
|
401
767
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
}
|
|
768
|
+
return matchedRoutes;
|
|
769
|
+
};
|
|
405
770
|
|
|
406
|
-
|
|
407
|
-
|
|
771
|
+
const getRoutePatternParts = (
|
|
772
|
+
path: string,
|
|
773
|
+
screen: string
|
|
774
|
+
): RoutePatternPart[] =>
|
|
775
|
+
getPatternParts(path).map((part) => {
|
|
776
|
+
const rank = getSegmentRank(part.segment);
|
|
408
777
|
|
|
409
|
-
|
|
778
|
+
if (part.name) {
|
|
779
|
+
return {
|
|
780
|
+
type: 'param',
|
|
781
|
+
screen,
|
|
782
|
+
segment: part.segment,
|
|
783
|
+
rank,
|
|
784
|
+
name: part.name,
|
|
785
|
+
...(part.pattern
|
|
786
|
+
? {
|
|
787
|
+
pattern: part.pattern,
|
|
788
|
+
regex: new RegExp(`^(?:${part.pattern})$`),
|
|
789
|
+
}
|
|
790
|
+
: null),
|
|
791
|
+
...(part.optional ? { optional: true } : null),
|
|
792
|
+
...(part.repeat ? { repeat: part.repeat } : null),
|
|
793
|
+
};
|
|
794
|
+
}
|
|
410
795
|
|
|
411
|
-
|
|
796
|
+
if (part.segment === '*') {
|
|
797
|
+
return {
|
|
798
|
+
type: 'wildcard',
|
|
799
|
+
segment: part.segment,
|
|
800
|
+
rank,
|
|
801
|
+
};
|
|
412
802
|
}
|
|
413
|
-
}
|
|
414
803
|
|
|
415
|
-
|
|
416
|
-
|
|
804
|
+
return {
|
|
805
|
+
type: 'static',
|
|
806
|
+
segment: part.segment,
|
|
807
|
+
rank,
|
|
808
|
+
pattern: getStaticSegmentPattern(part.segment),
|
|
809
|
+
};
|
|
810
|
+
});
|
|
417
811
|
|
|
418
|
-
const
|
|
812
|
+
const setupNormalizedConfigs = (
|
|
419
813
|
screen: string,
|
|
420
814
|
routeConfig: Record<string, string | PathConfig<{}>>,
|
|
421
815
|
initials: InitialRouteConfig[],
|
|
422
|
-
|
|
816
|
+
configs: RouteConfig[],
|
|
817
|
+
parts: RoutePatternPart[],
|
|
423
818
|
parentScreens: string[],
|
|
424
819
|
routeNames: string[]
|
|
425
|
-
):
|
|
426
|
-
const configs: RouteConfig[] = [];
|
|
427
|
-
|
|
820
|
+
): void => {
|
|
428
821
|
routeNames.push(screen);
|
|
429
822
|
|
|
430
823
|
parentScreens.push(screen);
|
|
@@ -432,19 +825,31 @@ const createNormalizedConfigs = (
|
|
|
432
825
|
const config = routeConfig[screen];
|
|
433
826
|
|
|
434
827
|
if (typeof config === 'string') {
|
|
435
|
-
|
|
436
|
-
|
|
828
|
+
const configParts = combinePatternParts(
|
|
829
|
+
parts,
|
|
830
|
+
getRoutePatternParts(config, screen)
|
|
831
|
+
);
|
|
832
|
+
|
|
833
|
+
configs.push(createConfigItem(screen, [...routeNames], configParts));
|
|
437
834
|
} else if (typeof config === 'object') {
|
|
438
835
|
// if an object is specified as the value (e.g. Foo: { ... }),
|
|
439
836
|
// it can have `path` property and
|
|
440
837
|
// it could have `screens` prop which has nested configs
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
838
|
+
const nestedScreens =
|
|
839
|
+
'screens' in config
|
|
840
|
+
? (config.screens as Record<string, string | PathConfig<{}>>)
|
|
841
|
+
: undefined;
|
|
842
|
+
const hasNestedScreens = !!nestedScreens;
|
|
843
|
+
const currentParts =
|
|
844
|
+
typeof config.path === 'string'
|
|
845
|
+
? combinePatternParts(
|
|
846
|
+
parts,
|
|
847
|
+
getRoutePatternParts(config.path, screen),
|
|
848
|
+
config.exact
|
|
849
|
+
)
|
|
850
|
+
: parts;
|
|
447
851
|
|
|
852
|
+
if (typeof config.path === 'string') {
|
|
448
853
|
// We should add alias configs after the main config
|
|
449
854
|
// So unless they are more specific, main config will be matched first
|
|
450
855
|
const aliasConfigs = [];
|
|
@@ -456,8 +861,10 @@ const createNormalizedConfigs = (
|
|
|
456
861
|
createConfigItem(
|
|
457
862
|
screen,
|
|
458
863
|
[...routeNames],
|
|
459
|
-
|
|
460
|
-
config.parse
|
|
864
|
+
combinePatternParts(parts, getRoutePatternParts(alias, screen)),
|
|
865
|
+
config.parse,
|
|
866
|
+
hasNestedScreens,
|
|
867
|
+
false
|
|
461
868
|
)
|
|
462
869
|
);
|
|
463
870
|
} else if (typeof alias === 'object') {
|
|
@@ -465,25 +872,29 @@ const createNormalizedConfigs = (
|
|
|
465
872
|
createConfigItem(
|
|
466
873
|
screen,
|
|
467
874
|
[...routeNames],
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
875
|
+
combinePatternParts(
|
|
876
|
+
parts,
|
|
877
|
+
getRoutePatternParts(alias.path, screen),
|
|
878
|
+
alias.exact
|
|
879
|
+
),
|
|
880
|
+
alias.parse,
|
|
881
|
+
hasNestedScreens,
|
|
882
|
+
alias.shared === true
|
|
472
883
|
)
|
|
473
884
|
);
|
|
474
885
|
}
|
|
475
886
|
}
|
|
476
887
|
}
|
|
477
888
|
|
|
478
|
-
if (config.exact) {
|
|
479
|
-
// If it's an exact path, we don't need to keep track of the parent screens
|
|
480
|
-
// So we can clear it
|
|
481
|
-
paths.length = 0;
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
paths.push({ screen, path: config.path });
|
|
485
889
|
configs.push(
|
|
486
|
-
createConfigItem(
|
|
890
|
+
createConfigItem(
|
|
891
|
+
screen,
|
|
892
|
+
[...routeNames],
|
|
893
|
+
currentParts,
|
|
894
|
+
config.parse,
|
|
895
|
+
hasNestedScreens,
|
|
896
|
+
config.shared === true
|
|
897
|
+
)
|
|
487
898
|
);
|
|
488
899
|
|
|
489
900
|
configs.push(...aliasConfigs);
|
|
@@ -499,7 +910,7 @@ const createNormalizedConfigs = (
|
|
|
499
910
|
);
|
|
500
911
|
}
|
|
501
912
|
|
|
502
|
-
if (
|
|
913
|
+
if (nestedScreens) {
|
|
503
914
|
// property `initialRouteName` without `screens` has no purpose
|
|
504
915
|
if (
|
|
505
916
|
'initialRouteName' in config &&
|
|
@@ -511,89 +922,189 @@ const createNormalizedConfigs = (
|
|
|
511
922
|
});
|
|
512
923
|
}
|
|
513
924
|
|
|
514
|
-
|
|
515
|
-
|
|
925
|
+
for (const nestedConfig in nestedScreens) {
|
|
926
|
+
setupNormalizedConfigs(
|
|
516
927
|
nestedConfig,
|
|
517
|
-
|
|
928
|
+
nestedScreens,
|
|
518
929
|
initials,
|
|
519
|
-
|
|
930
|
+
configs,
|
|
931
|
+
currentParts,
|
|
520
932
|
[...parentScreens],
|
|
521
933
|
routeNames
|
|
522
934
|
);
|
|
523
|
-
|
|
524
|
-
configs.push(...result);
|
|
525
|
-
});
|
|
935
|
+
}
|
|
526
936
|
}
|
|
527
937
|
}
|
|
528
938
|
|
|
529
939
|
routeNames.pop();
|
|
530
|
-
|
|
531
|
-
return configs;
|
|
532
940
|
};
|
|
533
941
|
|
|
534
942
|
const createConfigItem = (
|
|
535
943
|
screen: string,
|
|
536
944
|
routeNames: string[],
|
|
537
|
-
|
|
538
|
-
parse?: ParseConfig
|
|
945
|
+
parts: RoutePatternPart[],
|
|
946
|
+
parse?: ParseConfig,
|
|
947
|
+
hasNestedScreens = false,
|
|
948
|
+
shared = false
|
|
539
949
|
): RouteConfig => {
|
|
540
|
-
|
|
950
|
+
let regex: RegExp | undefined;
|
|
541
951
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
952
|
+
const segments: string[] = [];
|
|
953
|
+
const segmentRanks: number[] = [];
|
|
954
|
+
const params: RouteConfig['params'] = [];
|
|
955
|
+
const pathParamNames = new Set<string>();
|
|
546
956
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
.map((it, i) => {
|
|
551
|
-
if (it.param) {
|
|
552
|
-
const reg = it.regex || '[^/]+';
|
|
957
|
+
if (parts.length) {
|
|
958
|
+
let regexString = '^(';
|
|
959
|
+
let index = 0;
|
|
553
960
|
|
|
554
|
-
|
|
555
|
-
|
|
961
|
+
for (const part of parts) {
|
|
962
|
+
segments.push(part.segment);
|
|
963
|
+
segmentRanks.push(part.rank);
|
|
556
964
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
965
|
+
if (part.type === 'param') {
|
|
966
|
+
// A custom regex may not match the percent-encoded form of the value
|
|
967
|
+
// So also accept segments containing an encoded character and validate them after decoding
|
|
968
|
+
let reg = '[^/]+';
|
|
969
|
+
|
|
970
|
+
if (part.pattern) {
|
|
971
|
+
const encodedReg = `(?=[^/]*%[0-9A-F]{2})[^/]+`;
|
|
972
|
+
|
|
973
|
+
reg = part.repeat
|
|
974
|
+
? `(?:${encodedReg})|(?![^/]*%[0-9A-F]{2})(?:${part.pattern})`
|
|
975
|
+
: `(?:${part.pattern})|${encodedReg}`;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
if (part.repeat) {
|
|
979
|
+
const repeatedReg = `(?:(?:${reg})\\/)+`;
|
|
980
|
+
|
|
981
|
+
regexString += `(?<${PARAM_GROUP_PREFIX}${index}>${repeatedReg})${
|
|
982
|
+
part.repeat === 'zero-or-more' ? '?' : ''
|
|
983
|
+
}`;
|
|
984
|
+
} else {
|
|
985
|
+
regexString += `(((?<${PARAM_GROUP_PREFIX}${index}>${reg})\\/)${part.optional ? '?' : ''})`;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
params.push({
|
|
989
|
+
index,
|
|
990
|
+
screen: part.screen,
|
|
991
|
+
name: part.name,
|
|
992
|
+
regex: part.regex,
|
|
993
|
+
...(part.repeat ? { repeat: part.repeat } : {}),
|
|
994
|
+
});
|
|
995
|
+
|
|
996
|
+
if (part.screen === screen) {
|
|
997
|
+
pathParamNames.add(part.name);
|
|
998
|
+
}
|
|
999
|
+
} else if (part.type === 'wildcard') {
|
|
1000
|
+
regexString += `.*\\/`;
|
|
1001
|
+
} else {
|
|
1002
|
+
regexString += `${part.pattern}\\/`;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
index++;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
regexString += ')$';
|
|
1009
|
+
regex = new RegExp(regexString);
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
const pattern = segments.join('/');
|
|
575
1013
|
|
|
576
1014
|
return {
|
|
577
1015
|
screen,
|
|
1016
|
+
order: 0,
|
|
578
1017
|
regex,
|
|
1018
|
+
pattern,
|
|
579
1019
|
segments,
|
|
1020
|
+
segmentRanks,
|
|
580
1021
|
params,
|
|
581
1022
|
routeNames,
|
|
582
1023
|
parse,
|
|
1024
|
+
explicitParamNames: getExplicitParamNames(parse),
|
|
1025
|
+
pathParamNames,
|
|
1026
|
+
hasNestedScreens,
|
|
1027
|
+
shared,
|
|
583
1028
|
};
|
|
584
1029
|
};
|
|
585
1030
|
|
|
586
|
-
const
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
1031
|
+
const selectSharedConfig = (
|
|
1032
|
+
match: RouteConfig,
|
|
1033
|
+
candidates: RouteConfig[],
|
|
1034
|
+
previous: NavigationState | PartialState<NavigationState> | undefined,
|
|
1035
|
+
initialRoutes: InitialRouteConfig[]
|
|
1036
|
+
) => {
|
|
1037
|
+
// Walk the previous state down to the focused leaf to get the active route path
|
|
1038
|
+
const focusedRouteNames: string[] = [];
|
|
1039
|
+
|
|
1040
|
+
let current = previous;
|
|
1041
|
+
|
|
1042
|
+
while (current?.routes.length) {
|
|
1043
|
+
const index =
|
|
1044
|
+
typeof current.index === 'number'
|
|
1045
|
+
? current.index
|
|
1046
|
+
: current.routes.length - 1;
|
|
1047
|
+
|
|
1048
|
+
const route = current.routes[index];
|
|
1049
|
+
|
|
1050
|
+
if (route == null) {
|
|
1051
|
+
break;
|
|
593
1052
|
}
|
|
1053
|
+
|
|
1054
|
+
focusedRouteNames.push(route.name);
|
|
1055
|
+
current = route.state;
|
|
594
1056
|
}
|
|
595
1057
|
|
|
596
|
-
|
|
1058
|
+
// Rank a candidate by how far it aligns with the focused path, then by
|
|
1059
|
+
// how many of its levels match a configured initial route
|
|
1060
|
+
const score = (routeNames: string[]) => {
|
|
1061
|
+
let focused = 0;
|
|
1062
|
+
|
|
1063
|
+
while (
|
|
1064
|
+
routeNames[focused] !== undefined &&
|
|
1065
|
+
routeNames[focused] === focusedRouteNames[focused]
|
|
1066
|
+
) {
|
|
1067
|
+
focused++;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
let initial = 0;
|
|
1071
|
+
|
|
1072
|
+
for (const config of initialRoutes) {
|
|
1073
|
+
if (
|
|
1074
|
+
arrayStartsWith(routeNames, config.parentScreens) &&
|
|
1075
|
+
routeNames[config.parentScreens.length] === config.initialRouteName
|
|
1076
|
+
) {
|
|
1077
|
+
initial++;
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
return { focused, initial };
|
|
1082
|
+
};
|
|
1083
|
+
|
|
1084
|
+
let selected = match;
|
|
1085
|
+
let best = { focused: -1, initial: -1 };
|
|
1086
|
+
|
|
1087
|
+
for (const candidate of candidates) {
|
|
1088
|
+
if (!candidate.shared) {
|
|
1089
|
+
continue;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
const { focused, initial } = score(candidate.routeNames);
|
|
1093
|
+
|
|
1094
|
+
const better =
|
|
1095
|
+
focused !== best.focused
|
|
1096
|
+
? focused > best.focused
|
|
1097
|
+
: initial !== best.initial
|
|
1098
|
+
? initial > best.initial
|
|
1099
|
+
: candidate.order < selected.order;
|
|
1100
|
+
|
|
1101
|
+
if (better) {
|
|
1102
|
+
selected = candidate;
|
|
1103
|
+
best = { focused, initial };
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
return selected;
|
|
597
1108
|
};
|
|
598
1109
|
|
|
599
1110
|
// Try to find an initial route connected with the one passed
|
|
@@ -606,7 +1117,14 @@ const findInitialRoute = (
|
|
|
606
1117
|
if (parentScreens.length === config.parentScreens.length) {
|
|
607
1118
|
let sameParents = true;
|
|
608
1119
|
for (let i = 0; i < parentScreens.length; i++) {
|
|
609
|
-
|
|
1120
|
+
const parentScreen = parentScreens[i];
|
|
1121
|
+
const configParentScreen = config.parentScreens[i];
|
|
1122
|
+
|
|
1123
|
+
if (
|
|
1124
|
+
parentScreen == null ||
|
|
1125
|
+
configParentScreen == null ||
|
|
1126
|
+
parentScreen.localeCompare(configParentScreen) !== 0
|
|
1127
|
+
) {
|
|
610
1128
|
sameParents = false;
|
|
611
1129
|
break;
|
|
612
1130
|
}
|
|
@@ -627,7 +1145,7 @@ const createStateObject = (
|
|
|
627
1145
|
initialRoute: string | undefined,
|
|
628
1146
|
route: ParsedRoute,
|
|
629
1147
|
isEmpty: boolean
|
|
630
|
-
):
|
|
1148
|
+
): PartialState<NavigationState> => {
|
|
631
1149
|
if (isEmpty) {
|
|
632
1150
|
if (initialRoute) {
|
|
633
1151
|
return {
|
|
@@ -657,75 +1175,184 @@ const createNestedStateObject = (
|
|
|
657
1175
|
path: string,
|
|
658
1176
|
routes: ParsedRoute[],
|
|
659
1177
|
initialRoutes: InitialRouteConfig[],
|
|
660
|
-
|
|
661
|
-
) => {
|
|
662
|
-
|
|
1178
|
+
routeConfig?: RouteConfig
|
|
1179
|
+
): PartialState<NavigationState> | undefined => {
|
|
1180
|
+
const focusedRoute = routes[routes.length - 1];
|
|
1181
|
+
|
|
1182
|
+
if (focusedRoute == null) {
|
|
1183
|
+
return undefined;
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
focusedRoute.path = path.replace(/\/$/, '');
|
|
1187
|
+
|
|
1188
|
+
const queryParams = parseQueryParams(
|
|
1189
|
+
path,
|
|
1190
|
+
routeConfig?.parse,
|
|
1191
|
+
routeConfig?.pathParamNames,
|
|
1192
|
+
routeConfig?.explicitParamNames,
|
|
1193
|
+
routeConfig?.hasNestedScreens,
|
|
1194
|
+
focusedRoute.params
|
|
1195
|
+
);
|
|
1196
|
+
|
|
1197
|
+
if (!queryParams.valid) {
|
|
1198
|
+
return undefined;
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
if (queryParams.params) {
|
|
1202
|
+
focusedRoute.params = { ...focusedRoute.params, ...queryParams.params };
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
let route = routes.shift();
|
|
1206
|
+
|
|
1207
|
+
if (route == null) {
|
|
1208
|
+
return undefined;
|
|
1209
|
+
}
|
|
1210
|
+
|
|
663
1211
|
const parentScreens: string[] = [];
|
|
664
1212
|
|
|
665
1213
|
let initialRoute = findInitialRoute(route.name, parentScreens, initialRoutes);
|
|
666
1214
|
|
|
667
1215
|
parentScreens.push(route.name);
|
|
668
1216
|
|
|
669
|
-
const state
|
|
670
|
-
initialRoute,
|
|
671
|
-
route,
|
|
672
|
-
routes.length === 0
|
|
673
|
-
);
|
|
1217
|
+
const state = createStateObject(initialRoute, route, routes.length === 0);
|
|
674
1218
|
|
|
675
1219
|
if (routes.length > 0) {
|
|
676
1220
|
let nestedState = state;
|
|
677
1221
|
|
|
678
|
-
|
|
1222
|
+
let nextRoute = routes.shift();
|
|
1223
|
+
|
|
1224
|
+
while (nextRoute != null) {
|
|
1225
|
+
route = nextRoute;
|
|
679
1226
|
initialRoute = findInitialRoute(route.name, parentScreens, initialRoutes);
|
|
680
1227
|
|
|
681
1228
|
const nestedStateIndex =
|
|
682
|
-
nestedState.index
|
|
1229
|
+
nestedState.index ?? nestedState.routes.length - 1;
|
|
1230
|
+
const nestedRoute = nestedState.routes[nestedStateIndex];
|
|
1231
|
+
|
|
1232
|
+
if (nestedRoute == null) {
|
|
1233
|
+
throw new Error(`Couldn't find a route at index ${nestedStateIndex}.`);
|
|
1234
|
+
}
|
|
683
1235
|
|
|
684
|
-
|
|
1236
|
+
nestedRoute.state = createStateObject(
|
|
685
1237
|
initialRoute,
|
|
686
1238
|
route,
|
|
687
1239
|
routes.length === 0
|
|
688
1240
|
);
|
|
689
1241
|
|
|
690
1242
|
if (routes.length > 0) {
|
|
691
|
-
|
|
692
|
-
|
|
1243
|
+
if (nestedRoute.state == null) {
|
|
1244
|
+
throw new Error(
|
|
1245
|
+
`Couldn't find nested state for route '${route.name}'.`
|
|
1246
|
+
);
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
nestedState = nestedRoute.state;
|
|
693
1250
|
}
|
|
694
1251
|
|
|
695
1252
|
parentScreens.push(route.name);
|
|
1253
|
+
nextRoute = routes.shift();
|
|
696
1254
|
}
|
|
697
1255
|
}
|
|
698
1256
|
|
|
699
|
-
route = findFocusedRoute(state) as ParsedRoute;
|
|
700
|
-
route.path = path.replace(/\/$/, '');
|
|
701
|
-
|
|
702
|
-
const params = parseQueryParams(
|
|
703
|
-
path,
|
|
704
|
-
flatConfig ? findParseConfigForRoute(route.name, flatConfig) : undefined
|
|
705
|
-
);
|
|
706
|
-
|
|
707
|
-
if (params) {
|
|
708
|
-
route.params = { ...route.params, ...params };
|
|
709
|
-
}
|
|
710
|
-
|
|
711
1257
|
return state;
|
|
712
1258
|
};
|
|
713
1259
|
|
|
714
|
-
const parseQueryParams = (
|
|
715
|
-
|
|
716
|
-
|
|
1260
|
+
const parseQueryParams = (
|
|
1261
|
+
path: string,
|
|
1262
|
+
parseConfig?: ParseConfig,
|
|
1263
|
+
pathParamNames: Set<string> = new Set(),
|
|
1264
|
+
explicitParamNames?: Set<string>,
|
|
1265
|
+
hasNestedScreens = false,
|
|
1266
|
+
routeParams?: object
|
|
1267
|
+
):
|
|
1268
|
+
| { valid: true; params?: Record<string, unknown> | undefined }
|
|
1269
|
+
| { valid: false } => {
|
|
1270
|
+
const queryIndex = path.indexOf('?');
|
|
1271
|
+
const query = queryIndex === -1 ? undefined : path.slice(queryIndex + 1);
|
|
1272
|
+
const params: Record<string, unknown> = query ? queryString.parse(query) : {};
|
|
1273
|
+
|
|
1274
|
+
// Path params should always win over same-named query params.
|
|
1275
|
+
for (const name of pathParamNames) {
|
|
1276
|
+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
|
1277
|
+
delete params[name];
|
|
1278
|
+
}
|
|
717
1279
|
|
|
718
1280
|
if (parseConfig) {
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
) {
|
|
725
|
-
params[name] = parseConfig[name](params[name]);
|
|
1281
|
+
for (const name in parseConfig) {
|
|
1282
|
+
const parser = parseConfig[name];
|
|
1283
|
+
|
|
1284
|
+
if (!parser || pathParamNames.has(name)) {
|
|
1285
|
+
continue;
|
|
726
1286
|
}
|
|
727
|
-
|
|
1287
|
+
|
|
1288
|
+
const schema = getStandardSchema(parser);
|
|
1289
|
+
|
|
1290
|
+
if (!Object.hasOwn(params, name)) {
|
|
1291
|
+
if (!schema) {
|
|
1292
|
+
continue;
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
const result = getValidationResult(schema, undefined);
|
|
1296
|
+
|
|
1297
|
+
if (result.issues) {
|
|
1298
|
+
return { valid: false };
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
if (result.value !== undefined) {
|
|
1302
|
+
params[name] = result.value;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
continue;
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
if (schema) {
|
|
1309
|
+
const result = getValidationResult(schema, params[name]);
|
|
1310
|
+
|
|
1311
|
+
if (result.issues) {
|
|
1312
|
+
return { valid: false };
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
params[name] = result.value;
|
|
1316
|
+
continue;
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
const value = Array.isArray(params[name])
|
|
1320
|
+
? params[name][0]
|
|
1321
|
+
: params[name];
|
|
1322
|
+
|
|
1323
|
+
if (typeof value !== 'string') {
|
|
1324
|
+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
|
1325
|
+
delete params[name];
|
|
1326
|
+
continue;
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
if (typeof parser === 'function') {
|
|
1330
|
+
params[name] = parser(value);
|
|
1331
|
+
continue;
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
throw new Error(INVALID_PARSER_ERROR);
|
|
1335
|
+
}
|
|
728
1336
|
}
|
|
729
1337
|
|
|
730
|
-
|
|
1338
|
+
if (
|
|
1339
|
+
hasNestedScreens &&
|
|
1340
|
+
!explicitParamNames?.has('screen') &&
|
|
1341
|
+
(typeof params.screen === 'string' ||
|
|
1342
|
+
(routeParams != null &&
|
|
1343
|
+
'screen' in routeParams &&
|
|
1344
|
+
typeof routeParams.screen === 'string'))
|
|
1345
|
+
) {
|
|
1346
|
+
for (const name of NESTED_SCREEN_PARAM_NAMES) {
|
|
1347
|
+
if (!explicitParamNames?.has(name)) {
|
|
1348
|
+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
|
1349
|
+
delete params[name];
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
return {
|
|
1355
|
+
valid: true,
|
|
1356
|
+
params: Object.keys(params).length ? params : undefined,
|
|
1357
|
+
};
|
|
731
1358
|
};
|