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