@react-navigation/core 8.0.0-alpha.3 → 8.0.0-alpha.30
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/lib/module/BaseNavigationContainer.js +34 -17
- 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 +127 -0
- package/lib/module/DataLoading.js.map +1 -0
- package/lib/module/NavigationBuilderContext.js +1 -0
- package/lib/module/NavigationBuilderContext.js.map +1 -1
- package/lib/module/NavigationIndependentTree.js +17 -8
- package/lib/module/NavigationIndependentTree.js.map +1 -1
- package/lib/module/NavigationProvider.js +22 -11
- package/lib/module/NavigationProvider.js.map +1 -1
- package/lib/module/NavigationStateContext.js.map +1 -1
- package/lib/module/PreventRemoveContext.js.map +1 -1
- package/lib/module/PreventRemoveProvider.js +37 -27
- package/lib/module/PreventRemoveProvider.js.map +1 -1
- package/lib/module/SceneView.js +7 -40
- package/lib/module/SceneView.js.map +1 -1
- package/lib/module/StaticNavigation.js +179 -50
- 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/checkSerializable.js +18 -13
- package/lib/module/checkSerializable.js.map +1 -1
- package/lib/module/createNavigationContainerRef.js +6 -8
- 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 +171 -90
- package/lib/module/getPathFromState.js.map +1 -1
- package/lib/module/getPatternParts.js +20 -4
- package/lib/module/getPatternParts.js.map +1 -1
- package/lib/module/getStateFromPath.js +485 -214
- package/lib/module/getStateFromPath.js.map +1 -1
- package/lib/module/getStateFromRouteParams.js +24 -0
- package/lib/module/getStateFromRouteParams.js.map +1 -0
- package/lib/module/index.js +5 -4
- package/lib/module/index.js.map +1 -1
- package/lib/module/theming/useTheme.js +1 -1
- package/lib/module/theming/useTheme.js.map +1 -1
- package/lib/module/types.js +35 -0
- package/lib/module/types.js.map +1 -1
- package/lib/module/useDescriptors.js +19 -11
- 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/useFocusEvents.js +12 -4
- package/lib/module/useFocusEvents.js.map +1 -1
- package/lib/module/useFocusedListenersChildrenAdapter.js +2 -2
- package/lib/module/useFocusedListenersChildrenAdapter.js.map +1 -1
- package/lib/module/useIsFocused.js +7 -12
- package/lib/module/useIsFocused.js.map +1 -1
- package/lib/module/useKeyedChildListeners.js +4 -2
- package/lib/module/useKeyedChildListeners.js.map +1 -1
- package/lib/module/useLazyValue.js.map +1 -1
- package/lib/module/useNavigation.js +2 -15
- package/lib/module/useNavigation.js.map +1 -1
- package/lib/module/useNavigationBuilder.js +201 -131
- package/lib/module/useNavigationBuilder.js.map +1 -1
- package/lib/module/useNavigationCache.js +22 -17
- package/lib/module/useNavigationCache.js.map +1 -1
- package/lib/module/useNavigationHelpers.js +28 -10
- 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 +70 -35
- package/lib/module/useNavigationState.js.map +1 -1
- package/lib/module/useOnAction.js +37 -7
- package/lib/module/useOnAction.js.map +1 -1
- package/lib/module/useOnGetState.js +6 -3
- package/lib/module/useOnGetState.js.map +1 -1
- package/lib/module/useOnPreventRemove.js +39 -30
- package/lib/module/useOnPreventRemove.js.map +1 -1
- package/lib/module/useOnRouteFocus.js +1 -1
- package/lib/module/useOnRouteFocus.js.map +1 -1
- package/lib/module/useOptionsGetters.js +10 -5
- package/lib/module/useOptionsGetters.js.map +1 -1
- package/lib/module/usePreventRemove.js +10 -3
- package/lib/module/usePreventRemove.js.map +1 -1
- package/lib/module/usePreventRemoveContext.js +1 -1
- package/lib/module/usePreventRemoveContext.js.map +1 -1
- package/lib/module/useRegisterNavigator.js +2 -2
- 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 +17 -11
- package/lib/module/useRouteCache.js.map +1 -1
- package/lib/module/useScheduleUpdate.js +9 -9
- 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 +7 -3
- 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 +9 -8
- 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 +12 -13
- package/lib/typescript/src/NavigationProvider.d.ts.map +1 -1
- 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 +3 -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 -109
- 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/checkSerializable.d.ts +8 -4
- package/lib/typescript/src/checkSerializable.d.ts.map +1 -1
- 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 -3
- 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 +1 -0
- package/lib/typescript/src/getPatternParts.d.ts.map +1 -1
- package/lib/typescript/src/getStateFromPath.d.ts +5 -5
- 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/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 +260 -159
- 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/useComponent.d.ts +1 -1
- package/lib/typescript/src/useComponent.d.ts.map +1 -1
- package/lib/typescript/src/useDescriptors.d.ts +8 -8
- 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/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 +3 -0
- package/lib/typescript/src/useIsFocused.d.ts.map +1 -1
- package/lib/typescript/src/useKeyedChildListeners.d.ts +3 -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 +23 -30
- package/lib/typescript/src/useNavigationBuilder.d.ts.map +1 -1
- package/lib/typescript/src/useNavigationCache.d.ts +3 -3
- 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 +19 -27
- package/lib/typescript/src/useNavigationHelpers.d.ts.map +1 -1
- package/lib/typescript/src/useNavigationState.d.ts +13 -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/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 +1 -1
- 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/useRoute.d.ts +7 -3
- package/lib/typescript/src/useRoute.d.ts.map +1 -1
- package/lib/typescript/src/useRouteCache.d.ts +5 -5
- 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.map +1 -1
- package/lib/typescript/src/utilities.d.ts +74 -37
- package/lib/typescript/src/utilities.d.ts.map +1 -1
- package/lib/typescript/src/validatePathConfig.d.ts.map +1 -1
- package/package.json +23 -22
- package/src/BaseNavigationContainer.tsx +351 -333
- package/src/ConsumedParamsContext.tsx +5 -0
- package/src/DataLoading.tsx +203 -0
- package/src/NavigationBuilderContext.tsx +14 -9
- package/src/NavigationFocusedRouteStateContext.tsx +4 -4
- package/src/NavigationIndependentTree.tsx +18 -12
- package/src/NavigationProvider.tsx +26 -15
- package/src/NavigationStateContext.tsx +5 -6
- package/src/PreventRemoveContext.tsx +1 -0
- package/src/PreventRemoveProvider.tsx +51 -46
- package/src/SceneView.tsx +7 -39
- package/src/StaticNavigation.tsx +659 -307
- package/src/StaticTreeContext.tsx +7 -0
- package/src/checkSerializable.tsx +35 -19
- package/src/createNavigationContainerRef.tsx +12 -9
- package/src/createNavigatorFactory.tsx +92 -8
- package/src/findFocusedRoute.tsx +13 -8
- package/src/getActionFromState.tsx +86 -51
- package/src/getPathFromState.tsx +254 -128
- package/src/getPatternParts.tsx +25 -4
- package/src/getStateFromPath.tsx +811 -309
- package/src/getStateFromRouteParams.tsx +60 -0
- package/src/index.tsx +55 -5
- package/src/theming/useTheme.tsx +1 -1
- package/src/types.tsx +798 -373
- package/src/useDescriptors.tsx +28 -14
- package/src/useEventEmitter.tsx +52 -38
- package/src/useFocusEvents.tsx +15 -7
- package/src/useFocusedListenersChildrenAdapter.tsx +2 -2
- package/src/useIsFocused.tsx +14 -21
- package/src/useKeyedChildListeners.tsx +4 -2
- package/src/useLazyValue.tsx +2 -2
- package/src/useNavigation.tsx +7 -24
- package/src/useNavigationBuilder.tsx +311 -186
- package/src/useNavigationCache.tsx +32 -25
- package/src/useNavigationHelpers.tsx +27 -10
- package/src/useNavigationIndependentTree.tsx +1 -1
- package/src/useNavigationState.tsx +93 -59
- package/src/useOnAction.tsx +61 -13
- package/src/useOnGetState.tsx +5 -3
- package/src/useOnPreventRemove.tsx +75 -42
- package/src/useOnRouteFocus.tsx +10 -12
- package/src/useOptionsGetters.tsx +11 -5
- package/src/usePreventRemove.tsx +12 -3
- package/src/usePreventRemoveContext.tsx +1 -1
- package/src/useRegisterNavigator.tsx +2 -2
- package/src/useRoute.tsx +20 -6
- package/src/useRouteCache.tsx +22 -16
- package/src/useScheduleUpdate.tsx +9 -10
- package/src/useStateForPath.tsx +1 -1
- package/src/useSyncState.tsx +8 -3
- package/src/utilities.tsx +144 -31
- package/src/validatePathConfig.tsx +86 -57
- package/lib/module/CurrentRenderContext.js +0 -10
- package/lib/module/CurrentRenderContext.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/useCurrentRender.d.ts +0 -14
- package/lib/typescript/src/useCurrentRender.d.ts.map +0 -1
- package/src/CurrentRenderContext.tsx +0 -9
- package/src/useCurrentRender.tsx +0 -31
package/src/getStateFromPath.tsx
CHANGED
|
@@ -4,30 +4,53 @@ import type {
|
|
|
4
4
|
PartialState,
|
|
5
5
|
} from '@react-navigation/routers';
|
|
6
6
|
import escape from 'escape-string-regexp';
|
|
7
|
-
import
|
|
7
|
+
import queryString from 'query-string';
|
|
8
8
|
|
|
9
9
|
import { arrayStartsWith } from './arrayStartsWith';
|
|
10
10
|
import { findFocusedRoute } from './findFocusedRoute';
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
combinePatternParts,
|
|
13
|
+
getPatternParts,
|
|
14
|
+
type PatternPart,
|
|
15
|
+
} from './getPatternParts';
|
|
12
16
|
import { isArrayEqual } from './isArrayEqual';
|
|
13
17
|
import type { PathConfig, PathConfigMap } from './types';
|
|
18
|
+
import type {
|
|
19
|
+
StandardSchemaV1,
|
|
20
|
+
StandardSchemaValidationResult,
|
|
21
|
+
} from './utilities';
|
|
14
22
|
import { validatePathConfig } from './validatePathConfig';
|
|
15
23
|
|
|
16
24
|
type Options<ParamList extends {}> = {
|
|
17
|
-
path?: string;
|
|
18
|
-
initialRouteName?: string;
|
|
25
|
+
path?: string | undefined;
|
|
26
|
+
initialRouteName?: string | undefined;
|
|
19
27
|
screens: PathConfigMap<ParamList>;
|
|
20
28
|
};
|
|
21
29
|
|
|
22
|
-
type
|
|
30
|
+
type ParseConfigValue =
|
|
31
|
+
| ((value: string) => unknown)
|
|
32
|
+
| StandardSchemaV1<unknown, unknown>;
|
|
33
|
+
|
|
34
|
+
type ParseConfig = Record<string, ParseConfigValue | undefined>;
|
|
23
35
|
|
|
24
36
|
type RouteConfig = {
|
|
25
37
|
screen: string;
|
|
26
|
-
|
|
38
|
+
order: number;
|
|
39
|
+
regex?: RegExp | undefined;
|
|
40
|
+
pattern: string;
|
|
27
41
|
segments: string[];
|
|
28
|
-
params: {
|
|
42
|
+
params: {
|
|
43
|
+
screen: string;
|
|
44
|
+
name: string;
|
|
45
|
+
index: number;
|
|
46
|
+
regex?: RegExp | undefined;
|
|
47
|
+
}[];
|
|
29
48
|
routeNames: string[];
|
|
30
|
-
parse?: ParseConfig;
|
|
49
|
+
parse?: ParseConfig | undefined;
|
|
50
|
+
explicitParamNames?: Set<string> | undefined;
|
|
51
|
+
pathParamNames: Set<string>;
|
|
52
|
+
hasNestedScreens: boolean;
|
|
53
|
+
shared: boolean;
|
|
31
54
|
};
|
|
32
55
|
|
|
33
56
|
type InitialRouteConfig = {
|
|
@@ -36,18 +59,97 @@ type InitialRouteConfig = {
|
|
|
36
59
|
};
|
|
37
60
|
|
|
38
61
|
type ResultState = PartialState<NavigationState> & {
|
|
39
|
-
state?: ResultState;
|
|
62
|
+
state?: ResultState | undefined;
|
|
40
63
|
};
|
|
41
64
|
|
|
42
65
|
type ParsedRoute = {
|
|
43
66
|
name: string;
|
|
44
|
-
path?: string;
|
|
45
67
|
params?: Record<string, unknown> | undefined;
|
|
46
68
|
};
|
|
47
69
|
|
|
70
|
+
type RoutePatternPart = PatternPart & { screen: string };
|
|
71
|
+
|
|
48
72
|
type ConfigResources = {
|
|
49
73
|
initialRoutes: InitialRouteConfig[];
|
|
50
74
|
configs: RouteConfig[];
|
|
75
|
+
configsByScreen: Record<string, RouteConfig[]>;
|
|
76
|
+
configsByPattern: Record<string, RouteConfig[]>;
|
|
77
|
+
prefixRegex?: RegExp | undefined;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const INVALID_SCHEMA_RESULT_ERROR =
|
|
81
|
+
'Invalid validation result from schema. It should be an object with either "value" or "issues" property and cannot be asynchronous.';
|
|
82
|
+
|
|
83
|
+
const INVALID_PARSER_ERROR =
|
|
84
|
+
'Invalid parser. Expected a function or a Standard Schema V1 object.';
|
|
85
|
+
|
|
86
|
+
const PARAM_GROUP_PREFIX = 'param_';
|
|
87
|
+
|
|
88
|
+
const NESTED_SCREEN_PARAM_NAMES = [
|
|
89
|
+
'screen',
|
|
90
|
+
'params',
|
|
91
|
+
'initial',
|
|
92
|
+
'path',
|
|
93
|
+
'merge',
|
|
94
|
+
'pop',
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
const getStaticSegmentPattern = (segment: string) =>
|
|
98
|
+
Array.from(segment, (char) => {
|
|
99
|
+
const encoded = encodeURIComponent(char);
|
|
100
|
+
const percentEncoded =
|
|
101
|
+
encoded === char
|
|
102
|
+
? `%${char.charCodeAt(0).toString(16).padStart(2, '0').toUpperCase()}`
|
|
103
|
+
: encoded;
|
|
104
|
+
|
|
105
|
+
return `(?:${escape(char)}|${escape(percentEncoded)})`;
|
|
106
|
+
}).join('');
|
|
107
|
+
|
|
108
|
+
const getExplicitParamNames = (parse?: ParseConfig) => {
|
|
109
|
+
const names: string[] = [];
|
|
110
|
+
|
|
111
|
+
if (parse) {
|
|
112
|
+
for (const name in parse) {
|
|
113
|
+
if (parse[name] != null) {
|
|
114
|
+
names.push(name);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return names.length ? new Set(names) : undefined;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const getStandardSchema = (parser: ParseConfigValue) => {
|
|
123
|
+
if (
|
|
124
|
+
'~standard' in parser &&
|
|
125
|
+
typeof parser['~standard'] === 'object' &&
|
|
126
|
+
parser['~standard'] !== null &&
|
|
127
|
+
'version' in parser['~standard'] &&
|
|
128
|
+
parser['~standard'].version === 1 &&
|
|
129
|
+
'validate' in parser['~standard'] &&
|
|
130
|
+
typeof parser['~standard'].validate === 'function'
|
|
131
|
+
) {
|
|
132
|
+
return parser['~standard'];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return undefined;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const getValidationResult = (
|
|
139
|
+
schema: StandardSchemaV1<unknown, unknown>['~standard'],
|
|
140
|
+
value: unknown
|
|
141
|
+
): StandardSchemaValidationResult<unknown> => {
|
|
142
|
+
const result = schema.validate(value);
|
|
143
|
+
|
|
144
|
+
if (
|
|
145
|
+
result != null &&
|
|
146
|
+
typeof result === 'object' &&
|
|
147
|
+
('value' in result || ('issues' in result && Array.isArray(result.issues)))
|
|
148
|
+
) {
|
|
149
|
+
return result;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
throw new Error(INVALID_SCHEMA_RESULT_ERROR);
|
|
51
153
|
};
|
|
52
154
|
|
|
53
155
|
/**
|
|
@@ -73,44 +175,67 @@ type ConfigResources = {
|
|
|
73
175
|
*/
|
|
74
176
|
export function getStateFromPath<ParamList extends {}>(
|
|
75
177
|
path: string,
|
|
76
|
-
options?: Options<ParamList
|
|
178
|
+
options?: Options<ParamList>,
|
|
179
|
+
previous?: NavigationState
|
|
77
180
|
): ResultState | undefined {
|
|
78
|
-
const {
|
|
181
|
+
const {
|
|
182
|
+
initialRoutes,
|
|
183
|
+
configs,
|
|
184
|
+
configsByScreen,
|
|
185
|
+
configsByPattern,
|
|
186
|
+
prefixRegex,
|
|
187
|
+
} = getConfigResources(options);
|
|
79
188
|
|
|
80
189
|
const screens = options?.screens;
|
|
81
190
|
|
|
191
|
+
// When a matched config is shared, the same pattern can resolve to multiple branches
|
|
192
|
+
// pick the one that best matches the current navigation state
|
|
193
|
+
const resolveSharedConfig = (config: RouteConfig) => {
|
|
194
|
+
if (config.shared) {
|
|
195
|
+
const candidates = configsByPattern[config.pattern];
|
|
196
|
+
|
|
197
|
+
if (candidates) {
|
|
198
|
+
return selectSharedConfig(config, candidates, previous, initialRoutes);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return config;
|
|
203
|
+
};
|
|
204
|
+
|
|
82
205
|
let remaining = path
|
|
83
206
|
.replace(/\/+/g, '/') // Replace multiple slash (//) with single ones
|
|
84
207
|
.replace(/^\//, '') // Remove extra leading slash
|
|
85
|
-
.replace(/\?.*$/, '')
|
|
208
|
+
.replace(/\?.*$/, '') // Remove query params which we will handle later
|
|
209
|
+
.replace(/%[0-9a-f]{2}/gi, (m) => m.toUpperCase()); // Normalize percent-encoding as hex digits are case-insensitive
|
|
86
210
|
|
|
87
211
|
// Make sure there is a trailing slash
|
|
88
212
|
remaining = remaining.endsWith('/') ? remaining : `${remaining}/`;
|
|
89
213
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if (prefix) {
|
|
93
|
-
// Make sure there is a trailing slash
|
|
94
|
-
const normalizedPrefix = prefix.endsWith('/') ? prefix : `${prefix}/`;
|
|
214
|
+
if (prefixRegex) {
|
|
215
|
+
const prefixMatch = remaining.match(prefixRegex);
|
|
95
216
|
|
|
96
|
-
|
|
97
|
-
if (!remaining.startsWith(normalizedPrefix)) {
|
|
217
|
+
if (prefixMatch == null) {
|
|
98
218
|
return undefined;
|
|
99
219
|
}
|
|
100
220
|
|
|
101
|
-
|
|
102
|
-
remaining = remaining.replace(normalizedPrefix, '');
|
|
221
|
+
remaining = remaining.slice(prefixMatch[0].length);
|
|
103
222
|
}
|
|
104
223
|
|
|
105
224
|
if (screens === undefined) {
|
|
106
225
|
// When no config is specified, use the path segments as route names
|
|
107
|
-
const routes =
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
226
|
+
const routes: ParsedRoute[] = [];
|
|
227
|
+
|
|
228
|
+
for (const segment of remaining.split('/')) {
|
|
229
|
+
if (!segment) {
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
routes.push({ name: decodeURIComponent(segment) });
|
|
235
|
+
} catch {
|
|
236
|
+
return undefined;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
114
239
|
|
|
115
240
|
if (routes.length) {
|
|
116
241
|
return createNestedStateObject(path, routes, initialRoutes);
|
|
@@ -122,41 +247,105 @@ export function getStateFromPath<ParamList extends {}>(
|
|
|
122
247
|
if (remaining === '/') {
|
|
123
248
|
// We need to add special handling of empty path so navigation to empty path also works
|
|
124
249
|
// When handling empty path, we should only look at the root level config
|
|
125
|
-
const match = configs.find((config) => config.
|
|
250
|
+
const match = configs.find((config) => config.pattern === '');
|
|
126
251
|
|
|
127
252
|
if (match) {
|
|
253
|
+
const config = resolveSharedConfig(match);
|
|
254
|
+
|
|
128
255
|
return createNestedStateObject(
|
|
129
256
|
path,
|
|
130
|
-
|
|
257
|
+
config.routeNames.map((name) => ({ name })),
|
|
131
258
|
initialRoutes,
|
|
132
|
-
|
|
259
|
+
config
|
|
133
260
|
);
|
|
134
261
|
}
|
|
135
262
|
|
|
136
263
|
return undefined;
|
|
137
264
|
}
|
|
138
265
|
|
|
139
|
-
let result: PartialState<NavigationState> | undefined;
|
|
140
|
-
let current: PartialState<NavigationState> | undefined;
|
|
141
|
-
|
|
142
266
|
// We match the whole path against the regex instead of segments
|
|
143
267
|
// This makes sure matches such as wildcard will catch any unmatched routes, even if nested
|
|
144
|
-
const
|
|
268
|
+
const firstRawSegment = remaining.split('/')[0];
|
|
269
|
+
|
|
270
|
+
let firstDecodedSegment: string | undefined;
|
|
145
271
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
272
|
+
try {
|
|
273
|
+
firstDecodedSegment =
|
|
274
|
+
firstRawSegment != null ? decodeURIComponent(firstRawSegment) : undefined;
|
|
275
|
+
} catch {
|
|
276
|
+
firstDecodedSegment = undefined;
|
|
151
277
|
}
|
|
152
278
|
|
|
153
|
-
|
|
154
|
-
|
|
279
|
+
for (const config of configs) {
|
|
280
|
+
// Regex matching is expensive, so skip configs that can't match this path.
|
|
281
|
+
if (
|
|
282
|
+
!canMatchFirstSegment(
|
|
283
|
+
config.segments[0],
|
|
284
|
+
firstDecodedSegment,
|
|
285
|
+
firstRawSegment
|
|
286
|
+
)
|
|
287
|
+
) {
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const routes = matchAgainstConfig(remaining, config, configsByScreen);
|
|
292
|
+
|
|
293
|
+
if (routes === undefined) {
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
let selectedConfig = resolveSharedConfig(config);
|
|
298
|
+
let selectedRoutes = routes;
|
|
299
|
+
|
|
300
|
+
if (selectedConfig !== config) {
|
|
301
|
+
const rematchedRoutes = matchAgainstConfig(
|
|
302
|
+
remaining,
|
|
303
|
+
selectedConfig,
|
|
304
|
+
configsByScreen
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
if (rematchedRoutes === undefined) {
|
|
308
|
+
selectedConfig = config;
|
|
309
|
+
} else {
|
|
310
|
+
selectedRoutes = rematchedRoutes;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const state = createNestedStateObject(
|
|
315
|
+
path,
|
|
316
|
+
selectedRoutes,
|
|
317
|
+
initialRoutes,
|
|
318
|
+
selectedConfig
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
if (state !== undefined) {
|
|
322
|
+
return state;
|
|
323
|
+
}
|
|
155
324
|
}
|
|
156
325
|
|
|
157
|
-
return
|
|
326
|
+
return undefined;
|
|
158
327
|
}
|
|
159
328
|
|
|
329
|
+
const canMatchFirstSegment = (
|
|
330
|
+
configSegment: string | undefined,
|
|
331
|
+
decodedSegment: string | undefined,
|
|
332
|
+
rawSegment: string | undefined
|
|
333
|
+
) => {
|
|
334
|
+
if (decodedSegment === undefined) {
|
|
335
|
+
return true;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (configSegment === undefined) {
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (configSegment === '*' || configSegment.startsWith(':')) {
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return configSegment === decodedSegment || configSegment === rawSegment;
|
|
347
|
+
};
|
|
348
|
+
|
|
160
349
|
/**
|
|
161
350
|
* Reference to the last used config resources. This is used to avoid recomputing the config resources when the options are the same.
|
|
162
351
|
*/
|
|
@@ -165,11 +354,17 @@ const cachedConfigResources = new WeakMap<Options<{}>, ConfigResources>();
|
|
|
165
354
|
function getConfigResources<ParamList extends {}>(
|
|
166
355
|
options: Options<ParamList> | undefined
|
|
167
356
|
) {
|
|
168
|
-
if (!options)
|
|
357
|
+
if (!options) {
|
|
358
|
+
return prepareConfigResources();
|
|
359
|
+
}
|
|
169
360
|
|
|
170
361
|
const cached = cachedConfigResources.get(options);
|
|
171
362
|
|
|
172
|
-
if (cached)
|
|
363
|
+
if (cached) {
|
|
364
|
+
return cached;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
validatePathConfig(options);
|
|
173
368
|
|
|
174
369
|
const resources = prepareConfigResources(options);
|
|
175
370
|
|
|
@@ -179,21 +374,44 @@ function getConfigResources<ParamList extends {}>(
|
|
|
179
374
|
}
|
|
180
375
|
|
|
181
376
|
function prepareConfigResources(options?: Options<{}>) {
|
|
182
|
-
if (options) {
|
|
183
|
-
validatePathConfig(options);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
377
|
const initialRoutes = getInitialRoutes(options);
|
|
187
378
|
const configs = getSortedNormalizedConfigs(initialRoutes, options?.screens);
|
|
379
|
+
const prefix = options?.path?.replace(/^\//, '');
|
|
380
|
+
|
|
381
|
+
let prefixRegex: RegExp | undefined;
|
|
382
|
+
|
|
383
|
+
if (prefix) {
|
|
384
|
+
const normalizedPrefix = prefix.endsWith('/') ? prefix : `${prefix}/`;
|
|
385
|
+
const prefixPattern = normalizedPrefix
|
|
386
|
+
.split('/')
|
|
387
|
+
.map(getStaticSegmentPattern)
|
|
388
|
+
.join('/');
|
|
389
|
+
|
|
390
|
+
prefixRegex = new RegExp(`^${prefixPattern}`);
|
|
391
|
+
}
|
|
188
392
|
|
|
189
|
-
|
|
393
|
+
const configsByScreen: Record<string, RouteConfig[]> = {};
|
|
394
|
+
const configsByPattern: Record<string, RouteConfig[]> = {};
|
|
190
395
|
|
|
191
|
-
const
|
|
396
|
+
for (const c of configs) {
|
|
397
|
+
(configsByScreen[c.screen] ??= []).push(c);
|
|
398
|
+
|
|
399
|
+
const existing = configsByPattern[c.pattern];
|
|
400
|
+
|
|
401
|
+
if (existing) {
|
|
402
|
+
checkForDuplicatedConfigs(existing[existing.length - 1], c);
|
|
403
|
+
existing.push(c);
|
|
404
|
+
} else {
|
|
405
|
+
configsByPattern[c.pattern] = [c];
|
|
406
|
+
}
|
|
407
|
+
}
|
|
192
408
|
|
|
193
409
|
return {
|
|
194
410
|
initialRoutes,
|
|
195
411
|
configs,
|
|
196
|
-
|
|
412
|
+
configsByScreen,
|
|
413
|
+
configsByPattern,
|
|
414
|
+
prefixRegex,
|
|
197
415
|
};
|
|
198
416
|
}
|
|
199
417
|
|
|
@@ -215,211 +433,267 @@ function getSortedNormalizedConfigs(
|
|
|
215
433
|
screens: Record<string, string | PathConfig<{}>> = {}
|
|
216
434
|
) {
|
|
217
435
|
// 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
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
436
|
+
const configs: RouteConfig[] = [];
|
|
437
|
+
|
|
438
|
+
for (const key in screens) {
|
|
439
|
+
configs.push(
|
|
440
|
+
...createNormalizedConfigs(key, screens, initialRoutes, [], [], [])
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
for (let order = 0; order < configs.length; order++) {
|
|
445
|
+
const config = configs[order];
|
|
446
|
+
|
|
447
|
+
if (config) {
|
|
448
|
+
config.order = order;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return configs.sort((a, b) => {
|
|
453
|
+
// Sort config from most specific to least specific:
|
|
454
|
+
// - more segments
|
|
455
|
+
// - static segments
|
|
456
|
+
// - params with regex
|
|
457
|
+
// - regular params
|
|
458
|
+
// - wildcard
|
|
459
|
+
|
|
460
|
+
// If 2 patterns are same, move the one with less route names up
|
|
461
|
+
// This is an error state, so it's only useful for consistent error messages
|
|
462
|
+
if (isArrayEqual(a.segments, b.segments)) {
|
|
463
|
+
if (
|
|
464
|
+
a.routeNames.length > b.routeNames.length &&
|
|
465
|
+
arrayStartsWith(a.routeNames, b.routeNames)
|
|
466
|
+
) {
|
|
241
467
|
return -1;
|
|
242
468
|
}
|
|
243
469
|
|
|
244
|
-
if (
|
|
470
|
+
if (
|
|
471
|
+
b.routeNames.length > a.routeNames.length &&
|
|
472
|
+
arrayStartsWith(b.routeNames, a.routeNames)
|
|
473
|
+
) {
|
|
245
474
|
return 1;
|
|
246
475
|
}
|
|
247
476
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
if (a.segments[i] == null) {
|
|
251
|
-
return 1;
|
|
252
|
-
}
|
|
477
|
+
return a.routeNames.length - b.routeNames.length || a.order - b.order;
|
|
478
|
+
}
|
|
253
479
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
480
|
+
// If one of the patterns starts with the other, it's more exhaustive
|
|
481
|
+
// So move it up
|
|
482
|
+
if (arrayStartsWith(a.segments, b.segments)) {
|
|
483
|
+
return -1;
|
|
484
|
+
}
|
|
258
485
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
const bParam = b.segments[i].startsWith(':');
|
|
263
|
-
const aRegex = aParam && a.segments[i].includes('(');
|
|
264
|
-
const bRegex = bParam && b.segments[i].includes('(');
|
|
486
|
+
if (arrayStartsWith(b.segments, a.segments)) {
|
|
487
|
+
return 1;
|
|
488
|
+
}
|
|
265
489
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
}
|
|
490
|
+
for (let i = 0; i < Math.max(a.segments.length, b.segments.length); i++) {
|
|
491
|
+
const aSegment = a.segments[i];
|
|
492
|
+
const bSegment = b.segments[i];
|
|
270
493
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
494
|
+
// if b is longer, b gets higher priority
|
|
495
|
+
if (aSegment == null) {
|
|
496
|
+
return 1;
|
|
497
|
+
}
|
|
275
498
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
499
|
+
// if a is longer, a gets higher priority
|
|
500
|
+
if (bSegment == null) {
|
|
501
|
+
return -1;
|
|
502
|
+
}
|
|
280
503
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
504
|
+
const aWildCard = aSegment === '*';
|
|
505
|
+
const bWildCard = bSegment === '*';
|
|
506
|
+
const aParam = aSegment.startsWith(':');
|
|
507
|
+
const bParam = bSegment.startsWith(':');
|
|
508
|
+
const aRegex = aParam && aSegment.includes('(');
|
|
509
|
+
const bRegex = bParam && bSegment.includes('(');
|
|
285
510
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
511
|
+
// if both are wildcard or regex, we compare next component
|
|
512
|
+
if ((aWildCard && bWildCard) || (aRegex && bRegex)) {
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
290
515
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
516
|
+
// if only a is wildcard, b gets higher priority
|
|
517
|
+
if (aWildCard && !bWildCard) {
|
|
518
|
+
return 1;
|
|
519
|
+
}
|
|
295
520
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
}
|
|
521
|
+
// if only b is wildcard, a gets higher priority
|
|
522
|
+
if (bWildCard && !aWildCard) {
|
|
523
|
+
return -1;
|
|
300
524
|
}
|
|
301
525
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
526
|
+
// If only a has a param, b gets higher priority
|
|
527
|
+
if (aParam && !bParam) {
|
|
528
|
+
return 1;
|
|
529
|
+
}
|
|
305
530
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
//
|
|
317
|
-
|
|
318
|
-
|
|
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
|
-
);
|
|
531
|
+
// If only b has a param, a gets higher priority
|
|
532
|
+
if (bParam && !aParam) {
|
|
533
|
+
return -1;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// if only a has regex, a gets higher priority
|
|
537
|
+
if (aRegex && !bRegex) {
|
|
538
|
+
return -1;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// if only b has regex, b gets higher priority
|
|
542
|
+
if (bRegex && !aRegex) {
|
|
543
|
+
return 1;
|
|
330
544
|
}
|
|
331
545
|
}
|
|
332
546
|
|
|
333
|
-
return
|
|
334
|
-
|
|
335
|
-
});
|
|
336
|
-
}, {});
|
|
547
|
+
return a.segments.length - b.segments.length;
|
|
548
|
+
});
|
|
337
549
|
}
|
|
338
550
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
551
|
+
// Throw if two configs resolve to the same pattern but conflicting screens
|
|
552
|
+
function checkForDuplicatedConfigs(
|
|
553
|
+
existing: RouteConfig | undefined,
|
|
554
|
+
config: RouteConfig
|
|
555
|
+
) {
|
|
556
|
+
if (!existing) {
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const a = existing.routeNames;
|
|
561
|
+
const b = config.routeNames;
|
|
562
|
+
|
|
563
|
+
// It's not a problem if the path string omitted from a inner most screen
|
|
564
|
+
// For example, it's ok if a path resolves to `A > B > C` or `A > B`
|
|
565
|
+
const intersects =
|
|
566
|
+
a.length > b.length ? arrayStartsWith(a, b) : arrayStartsWith(b, a);
|
|
567
|
+
|
|
568
|
+
if (!intersects && (!existing.shared || !config.shared)) {
|
|
569
|
+
throw new Error(
|
|
570
|
+
`Found conflicting screens with the same pattern. The pattern '${
|
|
571
|
+
config.pattern
|
|
572
|
+
}' resolves to both '${a.join(' > ')}' and '${b.join(
|
|
573
|
+
' > '
|
|
574
|
+
)}'. Patterns must be unique and cannot resolve to more than one screen unless shared: true is specified.`
|
|
575
|
+
);
|
|
576
|
+
}
|
|
345
577
|
}
|
|
346
578
|
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
|
|
579
|
+
const matchAgainstConfig = (
|
|
580
|
+
remaining: string,
|
|
581
|
+
config: RouteConfig,
|
|
582
|
+
configsByScreen: Record<string, RouteConfig[]>
|
|
583
|
+
) => {
|
|
584
|
+
if (!config.regex) {
|
|
585
|
+
return undefined;
|
|
586
|
+
}
|
|
350
587
|
|
|
351
|
-
|
|
352
|
-
for (const config of configs) {
|
|
353
|
-
if (!config.regex) {
|
|
354
|
-
continue;
|
|
355
|
-
}
|
|
588
|
+
const match = remaining.match(config.regex);
|
|
356
589
|
|
|
357
|
-
|
|
590
|
+
if (!match) {
|
|
591
|
+
return undefined;
|
|
592
|
+
}
|
|
358
593
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
routes = config.routeNames.map((routeName) => {
|
|
362
|
-
const routeConfig = configs.find((c) => {
|
|
363
|
-
// Check matching name AND pattern in case same screen is used at different levels in config
|
|
364
|
-
return (
|
|
365
|
-
c.screen === routeName &&
|
|
366
|
-
arrayStartsWith(config.segments, c.segments)
|
|
367
|
-
);
|
|
368
|
-
});
|
|
594
|
+
let validationFailed = false;
|
|
595
|
+
const matchedRoutes: ParsedRoute[] = [];
|
|
369
596
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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;
|
|
597
|
+
for (const [index, routeName] of config.routeNames.entries()) {
|
|
598
|
+
// Check matching name, route names and pattern in case same screen is used
|
|
599
|
+
// at different levels or branches in config
|
|
600
|
+
const routeConfig = configsByScreen[routeName]?.find(
|
|
601
|
+
(c) =>
|
|
602
|
+
index === c.routeNames.length - 1 &&
|
|
603
|
+
arrayStartsWith(config.routeNames, c.routeNames) &&
|
|
604
|
+
arrayStartsWith(config.segments, c.segments)
|
|
605
|
+
);
|
|
606
|
+
|
|
607
|
+
let params: Record<string, unknown> | undefined;
|
|
608
|
+
let hasParams = false;
|
|
609
|
+
|
|
610
|
+
if (routeConfig && match.groups) {
|
|
611
|
+
params = {};
|
|
401
612
|
|
|
402
|
-
|
|
403
|
-
|
|
613
|
+
for (const param of routeConfig.params) {
|
|
614
|
+
if (param.screen !== routeName) {
|
|
615
|
+
continue;
|
|
404
616
|
}
|
|
405
617
|
|
|
406
|
-
|
|
407
|
-
});
|
|
618
|
+
const value = match.groups[`${PARAM_GROUP_PREFIX}${param.index}`];
|
|
408
619
|
|
|
409
|
-
|
|
620
|
+
if (value == null) {
|
|
621
|
+
params[param.name] = undefined;
|
|
622
|
+
hasParams = true;
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
410
625
|
|
|
411
|
-
|
|
626
|
+
let decoded: string;
|
|
627
|
+
|
|
628
|
+
try {
|
|
629
|
+
decoded = decodeURIComponent(value);
|
|
630
|
+
} catch {
|
|
631
|
+
return undefined;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Percent-encoded values are matched permissively
|
|
635
|
+
// So validate the decoded value against the custom regex
|
|
636
|
+
if (param.regex && value !== decoded && !param.regex.test(decoded)) {
|
|
637
|
+
return undefined;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
const parser = routeConfig.parse?.[param.name];
|
|
641
|
+
|
|
642
|
+
if (!parser) {
|
|
643
|
+
params[param.name] = decoded;
|
|
644
|
+
hasParams = true;
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
const schema = getStandardSchema(parser);
|
|
649
|
+
|
|
650
|
+
if (schema) {
|
|
651
|
+
const result = getValidationResult(schema, decoded);
|
|
652
|
+
|
|
653
|
+
if (result.issues) {
|
|
654
|
+
validationFailed = true;
|
|
655
|
+
break;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
params[param.name] = result.value;
|
|
659
|
+
hasParams = true;
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
if (typeof parser === 'function') {
|
|
664
|
+
params[param.name] = parser(decoded);
|
|
665
|
+
hasParams = true;
|
|
666
|
+
continue;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
throw new Error(INVALID_PARSER_ERROR);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
if (validationFailed) {
|
|
673
|
+
// A failed param validation invalidates the whole nested route chain for this config.
|
|
674
|
+
break;
|
|
675
|
+
}
|
|
412
676
|
}
|
|
677
|
+
|
|
678
|
+
if (params && hasParams) {
|
|
679
|
+
matchedRoutes.push({ name: routeName, params });
|
|
680
|
+
} else {
|
|
681
|
+
matchedRoutes.push({ name: routeName });
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
if (validationFailed) {
|
|
686
|
+
return undefined;
|
|
413
687
|
}
|
|
414
688
|
|
|
415
|
-
return
|
|
689
|
+
return matchedRoutes;
|
|
416
690
|
};
|
|
417
691
|
|
|
418
692
|
const createNormalizedConfigs = (
|
|
419
693
|
screen: string,
|
|
420
694
|
routeConfig: Record<string, string | PathConfig<{}>>,
|
|
421
695
|
initials: InitialRouteConfig[],
|
|
422
|
-
|
|
696
|
+
parts: RoutePatternPart[],
|
|
423
697
|
parentScreens: string[],
|
|
424
698
|
routeNames: string[]
|
|
425
699
|
): RouteConfig[] => {
|
|
@@ -432,19 +706,31 @@ const createNormalizedConfigs = (
|
|
|
432
706
|
const config = routeConfig[screen];
|
|
433
707
|
|
|
434
708
|
if (typeof config === 'string') {
|
|
435
|
-
|
|
436
|
-
|
|
709
|
+
const configParts = combinePatternParts(
|
|
710
|
+
parts,
|
|
711
|
+
getPatternParts(config).map((part) => ({ ...part, screen }))
|
|
712
|
+
);
|
|
713
|
+
|
|
714
|
+
configs.push(createConfigItem(screen, [...routeNames], configParts));
|
|
437
715
|
} else if (typeof config === 'object') {
|
|
438
716
|
// if an object is specified as the value (e.g. Foo: { ... }),
|
|
439
717
|
// it can have `path` property and
|
|
440
718
|
// it could have `screens` prop which has nested configs
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
719
|
+
const nestedScreens =
|
|
720
|
+
'screens' in config
|
|
721
|
+
? (config.screens as Record<string, string | PathConfig<{}>>)
|
|
722
|
+
: undefined;
|
|
723
|
+
const hasNestedScreens = !!nestedScreens;
|
|
724
|
+
const currentParts =
|
|
725
|
+
typeof config.path === 'string'
|
|
726
|
+
? combinePatternParts(
|
|
727
|
+
parts,
|
|
728
|
+
getPatternParts(config.path).map((part) => ({ ...part, screen })),
|
|
729
|
+
config.exact
|
|
730
|
+
)
|
|
731
|
+
: parts;
|
|
447
732
|
|
|
733
|
+
if (typeof config.path === 'string') {
|
|
448
734
|
// We should add alias configs after the main config
|
|
449
735
|
// So unless they are more specific, main config will be matched first
|
|
450
736
|
const aliasConfigs = [];
|
|
@@ -456,8 +742,13 @@ const createNormalizedConfigs = (
|
|
|
456
742
|
createConfigItem(
|
|
457
743
|
screen,
|
|
458
744
|
[...routeNames],
|
|
459
|
-
|
|
460
|
-
|
|
745
|
+
combinePatternParts(
|
|
746
|
+
parts,
|
|
747
|
+
getPatternParts(alias).map((part) => ({ ...part, screen }))
|
|
748
|
+
),
|
|
749
|
+
config.parse,
|
|
750
|
+
hasNestedScreens,
|
|
751
|
+
false
|
|
461
752
|
)
|
|
462
753
|
);
|
|
463
754
|
} else if (typeof alias === 'object') {
|
|
@@ -465,25 +756,32 @@ const createNormalizedConfigs = (
|
|
|
465
756
|
createConfigItem(
|
|
466
757
|
screen,
|
|
467
758
|
[...routeNames],
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
759
|
+
combinePatternParts(
|
|
760
|
+
parts,
|
|
761
|
+
getPatternParts(alias.path).map((part) => ({
|
|
762
|
+
...part,
|
|
763
|
+
screen,
|
|
764
|
+
})),
|
|
765
|
+
alias.exact
|
|
766
|
+
),
|
|
767
|
+
alias.parse,
|
|
768
|
+
hasNestedScreens,
|
|
769
|
+
alias.shared === true
|
|
472
770
|
)
|
|
473
771
|
);
|
|
474
772
|
}
|
|
475
773
|
}
|
|
476
774
|
}
|
|
477
775
|
|
|
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
776
|
configs.push(
|
|
486
|
-
createConfigItem(
|
|
777
|
+
createConfigItem(
|
|
778
|
+
screen,
|
|
779
|
+
[...routeNames],
|
|
780
|
+
currentParts,
|
|
781
|
+
config.parse,
|
|
782
|
+
hasNestedScreens,
|
|
783
|
+
config.shared === true
|
|
784
|
+
)
|
|
487
785
|
);
|
|
488
786
|
|
|
489
787
|
configs.push(...aliasConfigs);
|
|
@@ -499,7 +797,7 @@ const createNormalizedConfigs = (
|
|
|
499
797
|
);
|
|
500
798
|
}
|
|
501
799
|
|
|
502
|
-
if (
|
|
800
|
+
if (nestedScreens) {
|
|
503
801
|
// property `initialRouteName` without `screens` has no purpose
|
|
504
802
|
if (
|
|
505
803
|
'initialRouteName' in config &&
|
|
@@ -511,18 +809,18 @@ const createNormalizedConfigs = (
|
|
|
511
809
|
});
|
|
512
810
|
}
|
|
513
811
|
|
|
514
|
-
|
|
812
|
+
for (const nestedConfig in nestedScreens) {
|
|
515
813
|
const result = createNormalizedConfigs(
|
|
516
814
|
nestedConfig,
|
|
517
|
-
|
|
815
|
+
nestedScreens,
|
|
518
816
|
initials,
|
|
519
|
-
|
|
817
|
+
currentParts,
|
|
520
818
|
[...parentScreens],
|
|
521
819
|
routeNames
|
|
522
820
|
);
|
|
523
821
|
|
|
524
822
|
configs.push(...result);
|
|
525
|
-
}
|
|
823
|
+
}
|
|
526
824
|
}
|
|
527
825
|
}
|
|
528
826
|
|
|
@@ -534,66 +832,150 @@ const createNormalizedConfigs = (
|
|
|
534
832
|
const createConfigItem = (
|
|
535
833
|
screen: string,
|
|
536
834
|
routeNames: string[],
|
|
537
|
-
|
|
538
|
-
parse?: ParseConfig
|
|
835
|
+
parts: RoutePatternPart[],
|
|
836
|
+
parse?: ParseConfig,
|
|
837
|
+
hasNestedScreens = false,
|
|
838
|
+
shared = false
|
|
539
839
|
): RouteConfig => {
|
|
540
|
-
|
|
840
|
+
let regex: RegExp | undefined;
|
|
841
|
+
|
|
842
|
+
const segments: string[] = [];
|
|
843
|
+
const params: RouteConfig['params'] = [];
|
|
844
|
+
const pathParamNames = new Set<string>();
|
|
845
|
+
|
|
846
|
+
if (parts.length) {
|
|
847
|
+
let regexString = '^(';
|
|
848
|
+
let index = 0;
|
|
849
|
+
|
|
850
|
+
for (const part of parts) {
|
|
851
|
+
segments.push(part.segment);
|
|
852
|
+
|
|
853
|
+
if (part.param) {
|
|
854
|
+
// A custom regex may not match the percent-encoded form of the value
|
|
855
|
+
// So also accept segments containing an encoded character and validate them after decoding
|
|
856
|
+
const reg = part.regex
|
|
857
|
+
? `(?:${part.regex})|(?=[^/]*%[0-9A-F]{2})[^/]+`
|
|
858
|
+
: '[^/]+';
|
|
859
|
+
|
|
860
|
+
regexString += `(((?<${PARAM_GROUP_PREFIX}${index}>${reg})\\/)${part.optional ? '?' : ''})`;
|
|
861
|
+
params.push({
|
|
862
|
+
index,
|
|
863
|
+
screen: part.screen,
|
|
864
|
+
name: part.param,
|
|
865
|
+
regex: part.regex ? new RegExp(`^(?:${part.regex})$`) : undefined,
|
|
866
|
+
});
|
|
541
867
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
868
|
+
if (part.screen === screen) {
|
|
869
|
+
pathParamNames.add(part.param);
|
|
870
|
+
}
|
|
871
|
+
} else if (part.segment === '*') {
|
|
872
|
+
regexString += `.*\\/`;
|
|
873
|
+
} else {
|
|
874
|
+
regexString += `${getStaticSegmentPattern(part.segment)}\\/`;
|
|
875
|
+
}
|
|
546
876
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
`^(${parts
|
|
550
|
-
.map((it, i) => {
|
|
551
|
-
if (it.param) {
|
|
552
|
-
const reg = it.regex || '[^/]+';
|
|
877
|
+
index++;
|
|
878
|
+
}
|
|
553
879
|
|
|
554
|
-
|
|
555
|
-
|
|
880
|
+
regexString += ')$';
|
|
881
|
+
regex = new RegExp(regexString);
|
|
882
|
+
}
|
|
556
883
|
|
|
557
|
-
|
|
558
|
-
})
|
|
559
|
-
.join('')})$`
|
|
560
|
-
)
|
|
561
|
-
: undefined;
|
|
562
|
-
|
|
563
|
-
const segments = parts.map((it) => it.segment);
|
|
564
|
-
const params = parts
|
|
565
|
-
.map((it, i) =>
|
|
566
|
-
it.param
|
|
567
|
-
? {
|
|
568
|
-
index: i,
|
|
569
|
-
screen: it.screen,
|
|
570
|
-
name: it.param,
|
|
571
|
-
}
|
|
572
|
-
: null
|
|
573
|
-
)
|
|
574
|
-
.filter((it) => it != null);
|
|
884
|
+
const pattern = segments.join('/');
|
|
575
885
|
|
|
576
886
|
return {
|
|
577
887
|
screen,
|
|
888
|
+
order: 0,
|
|
578
889
|
regex,
|
|
890
|
+
pattern,
|
|
579
891
|
segments,
|
|
580
892
|
params,
|
|
581
893
|
routeNames,
|
|
582
894
|
parse,
|
|
895
|
+
explicitParamNames: getExplicitParamNames(parse),
|
|
896
|
+
pathParamNames,
|
|
897
|
+
hasNestedScreens,
|
|
898
|
+
shared,
|
|
583
899
|
};
|
|
584
900
|
};
|
|
585
901
|
|
|
586
|
-
const
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
902
|
+
const selectSharedConfig = (
|
|
903
|
+
match: RouteConfig,
|
|
904
|
+
candidates: RouteConfig[],
|
|
905
|
+
previous: NavigationState | PartialState<NavigationState> | undefined,
|
|
906
|
+
initialRoutes: InitialRouteConfig[]
|
|
907
|
+
) => {
|
|
908
|
+
// Walk the previous state down to the focused leaf to get the active route path
|
|
909
|
+
const focusedRouteNames: string[] = [];
|
|
910
|
+
|
|
911
|
+
let current = previous;
|
|
912
|
+
|
|
913
|
+
while (current?.routes.length) {
|
|
914
|
+
const index =
|
|
915
|
+
typeof current.index === 'number'
|
|
916
|
+
? current.index
|
|
917
|
+
: current.routes.length - 1;
|
|
918
|
+
|
|
919
|
+
const route = current.routes[index];
|
|
920
|
+
|
|
921
|
+
if (route == null) {
|
|
922
|
+
break;
|
|
593
923
|
}
|
|
924
|
+
|
|
925
|
+
focusedRouteNames.push(route.name);
|
|
926
|
+
current = route.state;
|
|
594
927
|
}
|
|
595
928
|
|
|
596
|
-
|
|
929
|
+
// Rank a candidate by how far it aligns with the focused path, then by
|
|
930
|
+
// how many of its levels match a configured initial route
|
|
931
|
+
const score = (routeNames: string[]) => {
|
|
932
|
+
let focused = 0;
|
|
933
|
+
|
|
934
|
+
while (
|
|
935
|
+
routeNames[focused] !== undefined &&
|
|
936
|
+
routeNames[focused] === focusedRouteNames[focused]
|
|
937
|
+
) {
|
|
938
|
+
focused++;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
let initial = 0;
|
|
942
|
+
|
|
943
|
+
for (const config of initialRoutes) {
|
|
944
|
+
if (
|
|
945
|
+
arrayStartsWith(routeNames, config.parentScreens) &&
|
|
946
|
+
routeNames[config.parentScreens.length] === config.initialRouteName
|
|
947
|
+
) {
|
|
948
|
+
initial++;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
return { focused, initial };
|
|
953
|
+
};
|
|
954
|
+
|
|
955
|
+
let selected = match;
|
|
956
|
+
let best = { focused: -1, initial: -1 };
|
|
957
|
+
|
|
958
|
+
for (const candidate of candidates) {
|
|
959
|
+
if (!candidate.shared) {
|
|
960
|
+
continue;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
const { focused, initial } = score(candidate.routeNames);
|
|
964
|
+
|
|
965
|
+
const better =
|
|
966
|
+
focused !== best.focused
|
|
967
|
+
? focused > best.focused
|
|
968
|
+
: initial !== best.initial
|
|
969
|
+
? initial > best.initial
|
|
970
|
+
: candidate.order < selected.order;
|
|
971
|
+
|
|
972
|
+
if (better) {
|
|
973
|
+
selected = candidate;
|
|
974
|
+
best = { focused, initial };
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
return selected;
|
|
597
979
|
};
|
|
598
980
|
|
|
599
981
|
// Try to find an initial route connected with the one passed
|
|
@@ -606,7 +988,14 @@ const findInitialRoute = (
|
|
|
606
988
|
if (parentScreens.length === config.parentScreens.length) {
|
|
607
989
|
let sameParents = true;
|
|
608
990
|
for (let i = 0; i < parentScreens.length; i++) {
|
|
609
|
-
|
|
991
|
+
const parentScreen = parentScreens[i];
|
|
992
|
+
const configParentScreen = config.parentScreens[i];
|
|
993
|
+
|
|
994
|
+
if (
|
|
995
|
+
parentScreen == null ||
|
|
996
|
+
configParentScreen == null ||
|
|
997
|
+
parentScreen.localeCompare(configParentScreen) !== 0
|
|
998
|
+
) {
|
|
610
999
|
sameParents = false;
|
|
611
1000
|
break;
|
|
612
1001
|
}
|
|
@@ -657,9 +1046,14 @@ const createNestedStateObject = (
|
|
|
657
1046
|
path: string,
|
|
658
1047
|
routes: ParsedRoute[],
|
|
659
1048
|
initialRoutes: InitialRouteConfig[],
|
|
660
|
-
|
|
661
|
-
) => {
|
|
662
|
-
let route = routes.shift()
|
|
1049
|
+
routeConfig?: RouteConfig
|
|
1050
|
+
): InitialState | undefined => {
|
|
1051
|
+
let route = routes.shift();
|
|
1052
|
+
|
|
1053
|
+
if (route == null) {
|
|
1054
|
+
return undefined;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
663
1057
|
const parentScreens: string[] = [];
|
|
664
1058
|
|
|
665
1059
|
let initialRoute = findInitialRoute(route.name, parentScreens, initialRoutes);
|
|
@@ -675,57 +1069,165 @@ const createNestedStateObject = (
|
|
|
675
1069
|
if (routes.length > 0) {
|
|
676
1070
|
let nestedState = state;
|
|
677
1071
|
|
|
678
|
-
|
|
1072
|
+
let nextRoute = routes.shift();
|
|
1073
|
+
|
|
1074
|
+
while (nextRoute != null) {
|
|
1075
|
+
route = nextRoute;
|
|
679
1076
|
initialRoute = findInitialRoute(route.name, parentScreens, initialRoutes);
|
|
680
1077
|
|
|
681
1078
|
const nestedStateIndex =
|
|
682
1079
|
nestedState.index || nestedState.routes.length - 1;
|
|
1080
|
+
const nestedRoute = nestedState.routes[nestedStateIndex];
|
|
683
1081
|
|
|
684
|
-
|
|
1082
|
+
if (nestedRoute == null) {
|
|
1083
|
+
throw new Error(`Couldn't find a route at index ${nestedStateIndex}.`);
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
nestedRoute.state = createStateObject(
|
|
685
1087
|
initialRoute,
|
|
686
1088
|
route,
|
|
687
1089
|
routes.length === 0
|
|
688
1090
|
);
|
|
689
1091
|
|
|
690
1092
|
if (routes.length > 0) {
|
|
691
|
-
|
|
692
|
-
|
|
1093
|
+
if (nestedRoute.state == null) {
|
|
1094
|
+
throw new Error(
|
|
1095
|
+
`Couldn't find nested state for route '${route.name}'.`
|
|
1096
|
+
);
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
nestedState = nestedRoute.state;
|
|
693
1100
|
}
|
|
694
1101
|
|
|
695
1102
|
parentScreens.push(route.name);
|
|
1103
|
+
nextRoute = routes.shift();
|
|
696
1104
|
}
|
|
697
1105
|
}
|
|
698
1106
|
|
|
699
|
-
|
|
700
|
-
|
|
1107
|
+
const focusedRoute = findFocusedRoute(state);
|
|
1108
|
+
|
|
1109
|
+
if (focusedRoute == null) {
|
|
1110
|
+
return undefined;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
focusedRoute.path = path.replace(/\/$/, '');
|
|
701
1114
|
|
|
702
|
-
const
|
|
1115
|
+
const queryParams = parseQueryParams(
|
|
703
1116
|
path,
|
|
704
|
-
|
|
1117
|
+
routeConfig?.parse,
|
|
1118
|
+
routeConfig?.pathParamNames,
|
|
1119
|
+
routeConfig?.explicitParamNames,
|
|
1120
|
+
routeConfig?.hasNestedScreens,
|
|
1121
|
+
focusedRoute.params
|
|
705
1122
|
);
|
|
706
1123
|
|
|
707
|
-
if (
|
|
708
|
-
|
|
1124
|
+
if (!queryParams.valid) {
|
|
1125
|
+
return undefined;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
if (queryParams.params) {
|
|
1129
|
+
focusedRoute.params = { ...focusedRoute.params, ...queryParams.params };
|
|
709
1130
|
}
|
|
710
1131
|
|
|
711
1132
|
return state;
|
|
712
1133
|
};
|
|
713
1134
|
|
|
714
|
-
const parseQueryParams = (
|
|
715
|
-
|
|
716
|
-
|
|
1135
|
+
const parseQueryParams = (
|
|
1136
|
+
path: string,
|
|
1137
|
+
parseConfig?: ParseConfig,
|
|
1138
|
+
pathParamNames: Set<string> = new Set(),
|
|
1139
|
+
explicitParamNames?: Set<string>,
|
|
1140
|
+
hasNestedScreens = false,
|
|
1141
|
+
routeParams?: object
|
|
1142
|
+
):
|
|
1143
|
+
| { valid: true; params?: Record<string, unknown> | undefined }
|
|
1144
|
+
| { valid: false } => {
|
|
1145
|
+
const queryIndex = path.indexOf('?');
|
|
1146
|
+
const query = queryIndex === -1 ? undefined : path.slice(queryIndex + 1);
|
|
1147
|
+
const params: Record<string, unknown> = query ? queryString.parse(query) : {};
|
|
1148
|
+
|
|
1149
|
+
// Path params should always win over same-named query params.
|
|
1150
|
+
for (const name of pathParamNames) {
|
|
1151
|
+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
|
1152
|
+
delete params[name];
|
|
1153
|
+
}
|
|
717
1154
|
|
|
718
1155
|
if (parseConfig) {
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
) {
|
|
725
|
-
params[name] = parseConfig[name](params[name]);
|
|
1156
|
+
for (const name in parseConfig) {
|
|
1157
|
+
const parser = parseConfig[name];
|
|
1158
|
+
|
|
1159
|
+
if (!parser || pathParamNames.has(name)) {
|
|
1160
|
+
continue;
|
|
726
1161
|
}
|
|
727
|
-
|
|
1162
|
+
|
|
1163
|
+
const schema = getStandardSchema(parser);
|
|
1164
|
+
|
|
1165
|
+
if (!Object.hasOwn(params, name)) {
|
|
1166
|
+
if (!schema) {
|
|
1167
|
+
continue;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
const result = getValidationResult(schema, undefined);
|
|
1171
|
+
|
|
1172
|
+
if (result.issues) {
|
|
1173
|
+
return { valid: false };
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
if (result.value !== undefined) {
|
|
1177
|
+
params[name] = result.value;
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
continue;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
if (schema) {
|
|
1184
|
+
const result = getValidationResult(schema, params[name]);
|
|
1185
|
+
|
|
1186
|
+
if (result.issues) {
|
|
1187
|
+
return { valid: false };
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
params[name] = result.value;
|
|
1191
|
+
continue;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
const value = Array.isArray(params[name])
|
|
1195
|
+
? params[name][0]
|
|
1196
|
+
: params[name];
|
|
1197
|
+
|
|
1198
|
+
if (typeof value !== 'string') {
|
|
1199
|
+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
|
1200
|
+
delete params[name];
|
|
1201
|
+
continue;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
if (typeof parser === 'function') {
|
|
1205
|
+
params[name] = parser(value);
|
|
1206
|
+
continue;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
throw new Error(INVALID_PARSER_ERROR);
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
if (
|
|
1214
|
+
hasNestedScreens &&
|
|
1215
|
+
!explicitParamNames?.has('screen') &&
|
|
1216
|
+
(typeof params.screen === 'string' ||
|
|
1217
|
+
(routeParams != null &&
|
|
1218
|
+
'screen' in routeParams &&
|
|
1219
|
+
typeof routeParams.screen === 'string'))
|
|
1220
|
+
) {
|
|
1221
|
+
for (const name of NESTED_SCREEN_PARAM_NAMES) {
|
|
1222
|
+
if (!explicitParamNames?.has(name)) {
|
|
1223
|
+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
|
1224
|
+
delete params[name];
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
728
1227
|
}
|
|
729
1228
|
|
|
730
|
-
return
|
|
1229
|
+
return {
|
|
1230
|
+
valid: true,
|
|
1231
|
+
params: Object.keys(params).length ? params : undefined,
|
|
1232
|
+
};
|
|
731
1233
|
};
|