@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
|
@@ -1,12 +1,45 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
import escape from 'escape-string-regexp';
|
|
4
|
-
import
|
|
4
|
+
import queryString from 'query-string';
|
|
5
5
|
import { arrayStartsWith } from "./arrayStartsWith.js";
|
|
6
|
-
import {
|
|
7
|
-
import { getPatternParts } from "./getPatternParts.js";
|
|
6
|
+
import { combinePatternParts, getPatternParts } from "./getPatternParts.js";
|
|
8
7
|
import { isArrayEqual } from "./isArrayEqual.js";
|
|
9
8
|
import { validatePathConfig } from "./validatePathConfig.js";
|
|
9
|
+
const INVALID_SCHEMA_RESULT_ERROR = 'Invalid validation result from schema. It should be an object with either "value" or "issues" property and cannot be asynchronous.';
|
|
10
|
+
const INVALID_PARSER_ERROR = 'Invalid parser. Expected a function or a Standard Schema V1 object.';
|
|
11
|
+
const PARAM_GROUP_PREFIX = 'param_';
|
|
12
|
+
const NESTED_SCREEN_PARAM_NAMES = ['screen', 'params', 'initial', 'path', 'merge', 'pop'];
|
|
13
|
+
const getStaticSegmentPattern = segment => Array.from(segment, char => {
|
|
14
|
+
const encoded = encodeURIComponent(char);
|
|
15
|
+
const percentEncoded = encoded === char ? `%${char.charCodeAt(0).toString(16).padStart(2, '0').toUpperCase()}` : encoded;
|
|
16
|
+
return `(?:${escape(char)}|${escape(percentEncoded)})`;
|
|
17
|
+
}).join('');
|
|
18
|
+
const getExplicitParamNames = parse => {
|
|
19
|
+
const names = [];
|
|
20
|
+
if (parse) {
|
|
21
|
+
for (const name in parse) {
|
|
22
|
+
if (parse[name] != null) {
|
|
23
|
+
names.push(name);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return names.length ? new Set(names) : undefined;
|
|
28
|
+
};
|
|
29
|
+
const getStandardSchema = parser => {
|
|
30
|
+
if ('~standard' in parser && typeof parser['~standard'] === 'object' && parser['~standard'] !== null && 'version' in parser['~standard'] && parser['~standard'].version === 1 && 'validate' in parser['~standard'] && typeof parser['~standard'].validate === 'function') {
|
|
31
|
+
return parser['~standard'];
|
|
32
|
+
}
|
|
33
|
+
return undefined;
|
|
34
|
+
};
|
|
35
|
+
const getValidationResult = (schema, value) => {
|
|
36
|
+
const result = schema.validate(value);
|
|
37
|
+
if (result != null && typeof result === 'object' && ('value' in result || 'issues' in result && Array.isArray(result.issues))) {
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
throw new Error(INVALID_SCHEMA_RESULT_ERROR);
|
|
41
|
+
};
|
|
42
|
+
|
|
10
43
|
/**
|
|
11
44
|
* Utility to parse a path string to initial state object accepted by the container.
|
|
12
45
|
* This is useful for deep linking when we need to handle the incoming URL.
|
|
@@ -28,40 +61,56 @@ import { validatePathConfig } from "./validatePathConfig.js";
|
|
|
28
61
|
* @param path Path string to parse and convert, e.g. /foo/bar?count=42.
|
|
29
62
|
* @param options Extra options to fine-tune how to parse the path.
|
|
30
63
|
*/
|
|
31
|
-
export function getStateFromPath(path, options) {
|
|
64
|
+
export function getStateFromPath(path, options, previous) {
|
|
32
65
|
const {
|
|
33
66
|
initialRoutes,
|
|
34
|
-
configs
|
|
67
|
+
configs,
|
|
68
|
+
configsByScreen,
|
|
69
|
+
configsByPattern,
|
|
70
|
+
prefixRegex
|
|
35
71
|
} = getConfigResources(options);
|
|
36
72
|
const screens = options?.screens;
|
|
73
|
+
|
|
74
|
+
// When a matched config is shared, the same pattern can resolve to multiple branches
|
|
75
|
+
// pick the one that best matches the current navigation state
|
|
76
|
+
const resolveSharedConfig = config => {
|
|
77
|
+
if (config.shared) {
|
|
78
|
+
const candidates = configsByPattern[config.pattern];
|
|
79
|
+
if (candidates) {
|
|
80
|
+
return selectSharedConfig(config, candidates, previous, initialRoutes);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return config;
|
|
84
|
+
};
|
|
37
85
|
let remaining = path.replace(/\/+/g, '/') // Replace multiple slash (//) with single ones
|
|
38
86
|
.replace(/^\//, '') // Remove extra leading slash
|
|
39
|
-
.replace(/\?.*$/, '')
|
|
87
|
+
.replace(/\?.*$/, '') // Remove query params which we will handle later
|
|
88
|
+
.replace(/%[0-9a-f]{2}/gi, m => m.toUpperCase()); // Normalize percent-encoding as hex digits are case-insensitive
|
|
40
89
|
|
|
41
90
|
// Make sure there is a trailing slash
|
|
42
91
|
remaining = remaining.endsWith('/') ? remaining : `${remaining}/`;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
// Make sure there is a trailing slash
|
|
47
|
-
const normalizedPrefix = prefix.endsWith('/') ? prefix : `${prefix}/`;
|
|
48
|
-
|
|
49
|
-
// If the path doesn't start with the prefix, it's not a match
|
|
50
|
-
if (!remaining.startsWith(normalizedPrefix)) {
|
|
92
|
+
if (prefixRegex) {
|
|
93
|
+
const prefixMatch = remaining.match(prefixRegex);
|
|
94
|
+
if (prefixMatch == null) {
|
|
51
95
|
return undefined;
|
|
52
96
|
}
|
|
53
|
-
|
|
54
|
-
// Remove the prefix from the path
|
|
55
|
-
remaining = remaining.replace(normalizedPrefix, '');
|
|
97
|
+
remaining = remaining.slice(prefixMatch[0].length);
|
|
56
98
|
}
|
|
57
99
|
if (screens === undefined) {
|
|
58
100
|
// When no config is specified, use the path segments as route names
|
|
59
|
-
const routes =
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
|
|
101
|
+
const routes = [];
|
|
102
|
+
for (const segment of remaining.split('/')) {
|
|
103
|
+
if (!segment) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
routes.push({
|
|
108
|
+
name: decodeURIComponent(segment)
|
|
109
|
+
});
|
|
110
|
+
} catch {
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
65
114
|
if (routes.length) {
|
|
66
115
|
return createNestedStateObject(path, routes, initialRoutes);
|
|
67
116
|
}
|
|
@@ -70,59 +119,109 @@ export function getStateFromPath(path, options) {
|
|
|
70
119
|
if (remaining === '/') {
|
|
71
120
|
// We need to add special handling of empty path so navigation to empty path also works
|
|
72
121
|
// When handling empty path, we should only look at the root level config
|
|
73
|
-
const match = configs.find(config => config.
|
|
122
|
+
const match = configs.find(config => config.pattern === '');
|
|
74
123
|
if (match) {
|
|
75
|
-
|
|
124
|
+
const config = resolveSharedConfig(match);
|
|
125
|
+
return createNestedStateObject(path, config.routeNames.map(name => ({
|
|
76
126
|
name
|
|
77
|
-
})), initialRoutes,
|
|
127
|
+
})), initialRoutes, config);
|
|
78
128
|
}
|
|
79
129
|
return undefined;
|
|
80
130
|
}
|
|
81
|
-
let result;
|
|
82
|
-
let current;
|
|
83
131
|
|
|
84
132
|
// We match the whole path against the regex instead of segments
|
|
85
133
|
// This makes sure matches such as wildcard will catch any unmatched routes, even if nested
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
current = createNestedStateObject(path, routes, initialRoutes, configs);
|
|
93
|
-
remaining = remainingPath;
|
|
94
|
-
result = current;
|
|
95
|
-
}
|
|
96
|
-
if (current == null || result == null) {
|
|
97
|
-
return undefined;
|
|
134
|
+
const firstRawSegment = remaining.split('/')[0];
|
|
135
|
+
let firstDecodedSegment;
|
|
136
|
+
try {
|
|
137
|
+
firstDecodedSegment = firstRawSegment != null ? decodeURIComponent(firstRawSegment) : undefined;
|
|
138
|
+
} catch {
|
|
139
|
+
firstDecodedSegment = undefined;
|
|
98
140
|
}
|
|
99
|
-
|
|
141
|
+
for (const config of configs) {
|
|
142
|
+
// Regex matching is expensive, so skip configs that can't match this path.
|
|
143
|
+
if (!canMatchFirstSegment(config.segments[0], firstDecodedSegment, firstRawSegment)) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
const routes = matchAgainstConfig(remaining, config, configsByScreen);
|
|
147
|
+
if (routes === undefined) {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
let selectedConfig = resolveSharedConfig(config);
|
|
151
|
+
let selectedRoutes = routes;
|
|
152
|
+
if (selectedConfig !== config) {
|
|
153
|
+
const rematchedRoutes = matchAgainstConfig(remaining, selectedConfig, configsByScreen);
|
|
154
|
+
if (rematchedRoutes === undefined) {
|
|
155
|
+
selectedConfig = config;
|
|
156
|
+
} else {
|
|
157
|
+
selectedRoutes = rematchedRoutes;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const state = createNestedStateObject(path, selectedRoutes, initialRoutes, selectedConfig);
|
|
161
|
+
if (state !== undefined) {
|
|
162
|
+
return state;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return undefined;
|
|
100
166
|
}
|
|
167
|
+
const canMatchFirstSegment = (configSegment, decodedSegment, rawSegment) => {
|
|
168
|
+
if (decodedSegment === undefined) {
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
if (configSegment === undefined) {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
if (configSegment === '*' || configSegment.startsWith(':')) {
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
return configSegment === decodedSegment || configSegment === rawSegment;
|
|
178
|
+
};
|
|
101
179
|
|
|
102
180
|
/**
|
|
103
181
|
* Reference to the last used config resources. This is used to avoid recomputing the config resources when the options are the same.
|
|
104
182
|
*/
|
|
105
183
|
const cachedConfigResources = new WeakMap();
|
|
106
184
|
function getConfigResources(options) {
|
|
107
|
-
if (!options)
|
|
185
|
+
if (!options) {
|
|
186
|
+
return prepareConfigResources();
|
|
187
|
+
}
|
|
108
188
|
const cached = cachedConfigResources.get(options);
|
|
109
|
-
if (cached)
|
|
189
|
+
if (cached) {
|
|
190
|
+
return cached;
|
|
191
|
+
}
|
|
192
|
+
validatePathConfig(options);
|
|
110
193
|
const resources = prepareConfigResources(options);
|
|
111
194
|
cachedConfigResources.set(options, resources);
|
|
112
195
|
return resources;
|
|
113
196
|
}
|
|
114
197
|
function prepareConfigResources(options) {
|
|
115
|
-
if (options) {
|
|
116
|
-
validatePathConfig(options);
|
|
117
|
-
}
|
|
118
198
|
const initialRoutes = getInitialRoutes(options);
|
|
119
199
|
const configs = getSortedNormalizedConfigs(initialRoutes, options?.screens);
|
|
120
|
-
|
|
121
|
-
|
|
200
|
+
const prefix = options?.path?.replace(/^\//, '');
|
|
201
|
+
let prefixRegex;
|
|
202
|
+
if (prefix) {
|
|
203
|
+
const normalizedPrefix = prefix.endsWith('/') ? prefix : `${prefix}/`;
|
|
204
|
+
const prefixPattern = normalizedPrefix.split('/').map(getStaticSegmentPattern).join('/');
|
|
205
|
+
prefixRegex = new RegExp(`^${prefixPattern}`);
|
|
206
|
+
}
|
|
207
|
+
const configsByScreen = {};
|
|
208
|
+
const configsByPattern = {};
|
|
209
|
+
for (const c of configs) {
|
|
210
|
+
(configsByScreen[c.screen] ??= []).push(c);
|
|
211
|
+
const existing = configsByPattern[c.pattern];
|
|
212
|
+
if (existing) {
|
|
213
|
+
checkForDuplicatedConfigs(existing[existing.length - 1], c);
|
|
214
|
+
existing.push(c);
|
|
215
|
+
} else {
|
|
216
|
+
configsByPattern[c.pattern] = [c];
|
|
217
|
+
}
|
|
218
|
+
}
|
|
122
219
|
return {
|
|
123
220
|
initialRoutes,
|
|
124
221
|
configs,
|
|
125
|
-
|
|
222
|
+
configsByScreen,
|
|
223
|
+
configsByPattern,
|
|
224
|
+
prefixRegex
|
|
126
225
|
};
|
|
127
226
|
}
|
|
128
227
|
function getInitialRoutes(options) {
|
|
@@ -137,7 +236,17 @@ function getInitialRoutes(options) {
|
|
|
137
236
|
}
|
|
138
237
|
function getSortedNormalizedConfigs(initialRoutes, screens = {}) {
|
|
139
238
|
// Create a normalized configs array which will be easier to use
|
|
140
|
-
|
|
239
|
+
const configs = [];
|
|
240
|
+
for (const key in screens) {
|
|
241
|
+
configs.push(...createNormalizedConfigs(key, screens, initialRoutes, [], [], []));
|
|
242
|
+
}
|
|
243
|
+
for (let order = 0; order < configs.length; order++) {
|
|
244
|
+
const config = configs[order];
|
|
245
|
+
if (config) {
|
|
246
|
+
config.order = order;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return configs.sort((a, b) => {
|
|
141
250
|
// Sort config from most specific to least specific:
|
|
142
251
|
// - more segments
|
|
143
252
|
// - static segments
|
|
@@ -148,7 +257,13 @@ function getSortedNormalizedConfigs(initialRoutes, screens = {}) {
|
|
|
148
257
|
// If 2 patterns are same, move the one with less route names up
|
|
149
258
|
// This is an error state, so it's only useful for consistent error messages
|
|
150
259
|
if (isArrayEqual(a.segments, b.segments)) {
|
|
151
|
-
|
|
260
|
+
if (a.routeNames.length > b.routeNames.length && arrayStartsWith(a.routeNames, b.routeNames)) {
|
|
261
|
+
return -1;
|
|
262
|
+
}
|
|
263
|
+
if (b.routeNames.length > a.routeNames.length && arrayStartsWith(b.routeNames, a.routeNames)) {
|
|
264
|
+
return 1;
|
|
265
|
+
}
|
|
266
|
+
return a.routeNames.length - b.routeNames.length || a.order - b.order;
|
|
152
267
|
}
|
|
153
268
|
|
|
154
269
|
// If one of the patterns starts with the other, it's more exhaustive
|
|
@@ -160,21 +275,24 @@ function getSortedNormalizedConfigs(initialRoutes, screens = {}) {
|
|
|
160
275
|
return 1;
|
|
161
276
|
}
|
|
162
277
|
for (let i = 0; i < Math.max(a.segments.length, b.segments.length); i++) {
|
|
278
|
+
const aSegment = a.segments[i];
|
|
279
|
+
const bSegment = b.segments[i];
|
|
280
|
+
|
|
163
281
|
// if b is longer, b gets higher priority
|
|
164
|
-
if (
|
|
282
|
+
if (aSegment == null) {
|
|
165
283
|
return 1;
|
|
166
284
|
}
|
|
167
285
|
|
|
168
286
|
// if a is longer, a gets higher priority
|
|
169
|
-
if (
|
|
287
|
+
if (bSegment == null) {
|
|
170
288
|
return -1;
|
|
171
289
|
}
|
|
172
|
-
const aWildCard =
|
|
173
|
-
const bWildCard =
|
|
174
|
-
const aParam =
|
|
175
|
-
const bParam =
|
|
176
|
-
const aRegex = aParam &&
|
|
177
|
-
const bRegex = bParam &&
|
|
290
|
+
const aWildCard = aSegment === '*';
|
|
291
|
+
const bWildCard = bSegment === '*';
|
|
292
|
+
const aParam = aSegment.startsWith(':');
|
|
293
|
+
const bParam = bSegment.startsWith(':');
|
|
294
|
+
const aRegex = aParam && aSegment.includes('(');
|
|
295
|
+
const bRegex = bParam && bSegment.includes('(');
|
|
178
296
|
|
|
179
297
|
// if both are wildcard or regex, we compare next component
|
|
180
298
|
if (aWildCard && bWildCard || aRegex && bRegex) {
|
|
@@ -214,142 +332,154 @@ function getSortedNormalizedConfigs(initialRoutes, screens = {}) {
|
|
|
214
332
|
return a.segments.length - b.segments.length;
|
|
215
333
|
});
|
|
216
334
|
}
|
|
217
|
-
function checkForDuplicatedConfigs(configs) {
|
|
218
|
-
// Check for duplicate patterns in the config
|
|
219
|
-
configs.reduce((acc, config) => {
|
|
220
|
-
const pattern = config.segments.join('/');
|
|
221
|
-
if (acc[pattern]) {
|
|
222
|
-
const a = acc[pattern].routeNames;
|
|
223
|
-
const b = config.routeNames;
|
|
224
335
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
return Object.assign(acc, {
|
|
233
|
-
[pattern]: config
|
|
234
|
-
});
|
|
235
|
-
}, {});
|
|
236
|
-
}
|
|
237
|
-
function getConfigsWithRegexes(configs) {
|
|
238
|
-
return configs.map(c => ({
|
|
239
|
-
...c,
|
|
240
|
-
// Add `$` to the regex to make sure it matches till end of the path and not just beginning
|
|
241
|
-
regex: c.regex ? new RegExp(c.regex.source + '$') : undefined
|
|
242
|
-
}));
|
|
243
|
-
}
|
|
244
|
-
const matchAgainstConfigs = (remaining, configs) => {
|
|
245
|
-
let routes;
|
|
246
|
-
let remainingPath = remaining;
|
|
336
|
+
// Throw if two configs resolve to the same pattern but conflicting screens
|
|
337
|
+
function checkForDuplicatedConfigs(existing, config) {
|
|
338
|
+
if (!existing) {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
const a = existing.routeNames;
|
|
342
|
+
const b = config.routeNames;
|
|
247
343
|
|
|
248
|
-
//
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
}
|
|
253
|
-
|
|
344
|
+
// It's not a problem if the path string omitted from a inner most screen
|
|
345
|
+
// For example, it's ok if a path resolves to `A > B > C` or `A > B`
|
|
346
|
+
const intersects = a.length > b.length ? arrayStartsWith(a, b) : arrayStartsWith(b, a);
|
|
347
|
+
if (!intersects && (!existing.shared || !config.shared)) {
|
|
348
|
+
throw new Error(`Found conflicting screens with the same pattern. The pattern '${config.pattern}' resolves to both '${a.join(' > ')}' and '${b.join(' > ')}'. Patterns must be unique and cannot resolve to more than one screen unless shared: true is specified.`);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
const matchAgainstConfig = (remaining, config, configsByScreen) => {
|
|
352
|
+
if (!config.regex) {
|
|
353
|
+
return undefined;
|
|
354
|
+
}
|
|
355
|
+
const match = remaining.match(config.regex);
|
|
356
|
+
if (!match) {
|
|
357
|
+
return undefined;
|
|
358
|
+
}
|
|
359
|
+
let validationFailed = false;
|
|
360
|
+
const matchedRoutes = [];
|
|
361
|
+
for (const [index, routeName] of config.routeNames.entries()) {
|
|
362
|
+
// Check matching name, route names and pattern in case same screen is used
|
|
363
|
+
// at different levels or branches in config
|
|
364
|
+
const routeConfig = configsByScreen[routeName]?.find(c => index === c.routeNames.length - 1 && arrayStartsWith(config.routeNames, c.routeNames) && arrayStartsWith(config.segments, c.segments));
|
|
365
|
+
let params;
|
|
366
|
+
let hasParams = false;
|
|
367
|
+
if (routeConfig && match.groups) {
|
|
368
|
+
params = {};
|
|
369
|
+
for (const param of routeConfig.params) {
|
|
370
|
+
if (param.screen !== routeName) {
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
const value = match.groups[`${PARAM_GROUP_PREFIX}${param.index}`];
|
|
374
|
+
if (value == null) {
|
|
375
|
+
params[param.name] = undefined;
|
|
376
|
+
hasParams = true;
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
let decoded;
|
|
380
|
+
try {
|
|
381
|
+
decoded = decodeURIComponent(value);
|
|
382
|
+
} catch {
|
|
383
|
+
return undefined;
|
|
384
|
+
}
|
|
254
385
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
386
|
+
// Percent-encoded values are matched permissively
|
|
387
|
+
// So validate the decoded value against the custom regex
|
|
388
|
+
if (param.regex && value !== decoded && !param.regex.test(decoded)) {
|
|
389
|
+
return undefined;
|
|
390
|
+
}
|
|
391
|
+
const parser = routeConfig.parse?.[param.name];
|
|
392
|
+
if (!parser) {
|
|
393
|
+
params[param.name] = decoded;
|
|
394
|
+
hasParams = true;
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
const schema = getStandardSchema(parser);
|
|
398
|
+
if (schema) {
|
|
399
|
+
const result = getValidationResult(schema, decoded);
|
|
400
|
+
if (result.issues) {
|
|
401
|
+
validationFailed = true;
|
|
402
|
+
break;
|
|
272
403
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
}
|
|
277
|
-
if (
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
};
|
|
404
|
+
params[param.name] = result.value;
|
|
405
|
+
hasParams = true;
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
if (typeof parser === 'function') {
|
|
409
|
+
params[param.name] = parser(decoded);
|
|
410
|
+
hasParams = true;
|
|
411
|
+
continue;
|
|
282
412
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
413
|
+
throw new Error(INVALID_PARSER_ERROR);
|
|
414
|
+
}
|
|
415
|
+
if (validationFailed) {
|
|
416
|
+
// A failed param validation invalidates the whole nested route chain for this config.
|
|
417
|
+
break;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
if (params && hasParams) {
|
|
421
|
+
matchedRoutes.push({
|
|
422
|
+
name: routeName,
|
|
423
|
+
params
|
|
424
|
+
});
|
|
425
|
+
} else {
|
|
426
|
+
matchedRoutes.push({
|
|
427
|
+
name: routeName
|
|
286
428
|
});
|
|
287
|
-
remainingPath = remainingPath.replace(match[0], '');
|
|
288
|
-
break;
|
|
289
429
|
}
|
|
290
430
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
431
|
+
if (validationFailed) {
|
|
432
|
+
return undefined;
|
|
433
|
+
}
|
|
434
|
+
return matchedRoutes;
|
|
295
435
|
};
|
|
296
|
-
const createNormalizedConfigs = (screen, routeConfig, initials,
|
|
436
|
+
const createNormalizedConfigs = (screen, routeConfig, initials, parts, parentScreens, routeNames) => {
|
|
297
437
|
const configs = [];
|
|
298
438
|
routeNames.push(screen);
|
|
299
439
|
parentScreens.push(screen);
|
|
300
440
|
const config = routeConfig[screen];
|
|
301
441
|
if (typeof config === 'string') {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
});
|
|
306
|
-
configs.push(createConfigItem(screen, [...routeNames],
|
|
442
|
+
const configParts = combinePatternParts(parts, getPatternParts(config).map(part => ({
|
|
443
|
+
...part,
|
|
444
|
+
screen
|
|
445
|
+
})));
|
|
446
|
+
configs.push(createConfigItem(screen, [...routeNames], configParts));
|
|
307
447
|
} else if (typeof config === 'object') {
|
|
308
448
|
// if an object is specified as the value (e.g. Foo: { ... }),
|
|
309
449
|
// it can have `path` property and
|
|
310
450
|
// it could have `screens` prop which has nested configs
|
|
451
|
+
const nestedScreens = 'screens' in config ? config.screens : undefined;
|
|
452
|
+
const hasNestedScreens = !!nestedScreens;
|
|
453
|
+
const currentParts = typeof config.path === 'string' ? combinePatternParts(parts, getPatternParts(config.path).map(part => ({
|
|
454
|
+
...part,
|
|
455
|
+
screen
|
|
456
|
+
})), config.exact) : parts;
|
|
311
457
|
if (typeof config.path === 'string') {
|
|
312
|
-
if (config.exact && config.path == null) {
|
|
313
|
-
throw new Error(`Screen '${screen}' doesn't specify a 'path'. A 'path' needs to be specified when specifying 'exact: true'. If you don't want this screen in the URL, specify it as empty string, e.g. \`path: ''\`.`);
|
|
314
|
-
}
|
|
315
|
-
|
|
316
458
|
// We should add alias configs after the main config
|
|
317
459
|
// So unless they are more specific, main config will be matched first
|
|
318
460
|
const aliasConfigs = [];
|
|
319
461
|
if (config.alias) {
|
|
320
462
|
for (const alias of config.alias) {
|
|
321
463
|
if (typeof alias === 'string') {
|
|
322
|
-
aliasConfigs.push(createConfigItem(screen, [...routeNames],
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
}
|
|
464
|
+
aliasConfigs.push(createConfigItem(screen, [...routeNames], combinePatternParts(parts, getPatternParts(alias).map(part => ({
|
|
465
|
+
...part,
|
|
466
|
+
screen
|
|
467
|
+
}))), config.parse, hasNestedScreens, false));
|
|
326
468
|
} else if (typeof alias === 'object') {
|
|
327
|
-
aliasConfigs.push(createConfigItem(screen, [...routeNames], alias.
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
}
|
|
331
|
-
screen,
|
|
332
|
-
path: alias.path
|
|
333
|
-
}], alias.parse));
|
|
469
|
+
aliasConfigs.push(createConfigItem(screen, [...routeNames], combinePatternParts(parts, getPatternParts(alias.path).map(part => ({
|
|
470
|
+
...part,
|
|
471
|
+
screen
|
|
472
|
+
})), alias.exact), alias.parse, hasNestedScreens, alias.shared === true));
|
|
334
473
|
}
|
|
335
474
|
}
|
|
336
475
|
}
|
|
337
|
-
|
|
338
|
-
// If it's an exact path, we don't need to keep track of the parent screens
|
|
339
|
-
// So we can clear it
|
|
340
|
-
paths.length = 0;
|
|
341
|
-
}
|
|
342
|
-
paths.push({
|
|
343
|
-
screen,
|
|
344
|
-
path: config.path
|
|
345
|
-
});
|
|
346
|
-
configs.push(createConfigItem(screen, [...routeNames], [...paths], config.parse));
|
|
476
|
+
configs.push(createConfigItem(screen, [...routeNames], currentParts, config.parse, hasNestedScreens, config.shared === true));
|
|
347
477
|
configs.push(...aliasConfigs);
|
|
348
478
|
}
|
|
349
479
|
if (typeof config !== 'string' && typeof config.path !== 'string' && config.alias?.length) {
|
|
350
480
|
throw new Error(`Screen '${screen}' doesn't specify a 'path'. A 'path' needs to be specified in order to use 'alias'.`);
|
|
351
481
|
}
|
|
352
|
-
if (
|
|
482
|
+
if (nestedScreens) {
|
|
353
483
|
// property `initialRouteName` without `screens` has no purpose
|
|
354
484
|
if ('initialRouteName' in config && typeof config.initialRouteName === 'string') {
|
|
355
485
|
initials.push({
|
|
@@ -357,57 +487,120 @@ const createNormalizedConfigs = (screen, routeConfig, initials, paths, parentScr
|
|
|
357
487
|
parentScreens
|
|
358
488
|
});
|
|
359
489
|
}
|
|
360
|
-
|
|
361
|
-
const result = createNormalizedConfigs(nestedConfig,
|
|
490
|
+
for (const nestedConfig in nestedScreens) {
|
|
491
|
+
const result = createNormalizedConfigs(nestedConfig, nestedScreens, initials, currentParts, [...parentScreens], routeNames);
|
|
362
492
|
configs.push(...result);
|
|
363
|
-
}
|
|
493
|
+
}
|
|
364
494
|
}
|
|
365
495
|
}
|
|
366
496
|
routeNames.pop();
|
|
367
497
|
return configs;
|
|
368
498
|
};
|
|
369
|
-
const createConfigItem = (screen, routeNames,
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
499
|
+
const createConfigItem = (screen, routeNames, parts, parse, hasNestedScreens = false, shared = false) => {
|
|
500
|
+
let regex;
|
|
501
|
+
const segments = [];
|
|
502
|
+
const params = [];
|
|
503
|
+
const pathParamNames = new Set();
|
|
504
|
+
if (parts.length) {
|
|
505
|
+
let regexString = '^(';
|
|
506
|
+
let index = 0;
|
|
507
|
+
for (const part of parts) {
|
|
508
|
+
segments.push(part.segment);
|
|
509
|
+
if (part.param) {
|
|
510
|
+
// A custom regex may not match the percent-encoded form of the value
|
|
511
|
+
// So also accept segments containing an encoded character and validate them after decoding
|
|
512
|
+
const reg = part.regex ? `(?:${part.regex})|(?=[^/]*%[0-9A-F]{2})[^/]+` : '[^/]+';
|
|
513
|
+
regexString += `(((?<${PARAM_GROUP_PREFIX}${index}>${reg})\\/)${part.optional ? '?' : ''})`;
|
|
514
|
+
params.push({
|
|
515
|
+
index,
|
|
516
|
+
screen: part.screen,
|
|
517
|
+
name: part.param,
|
|
518
|
+
regex: part.regex ? new RegExp(`^(?:${part.regex})$`) : undefined
|
|
519
|
+
});
|
|
520
|
+
if (part.screen === screen) {
|
|
521
|
+
pathParamNames.add(part.param);
|
|
522
|
+
}
|
|
523
|
+
} else if (part.segment === '*') {
|
|
524
|
+
regexString += `.*\\/`;
|
|
525
|
+
} else {
|
|
526
|
+
regexString += `${getStaticSegmentPattern(part.segment)}\\/`;
|
|
527
|
+
}
|
|
528
|
+
index++;
|
|
529
|
+
}
|
|
530
|
+
regexString += ')$';
|
|
531
|
+
regex = new RegExp(regexString);
|
|
381
532
|
}
|
|
382
|
-
const
|
|
383
|
-
if (it.param) {
|
|
384
|
-
const reg = it.regex || '[^/]+';
|
|
385
|
-
return `(((?<param_${i}>${reg})\\/)${it.optional ? '?' : ''})`;
|
|
386
|
-
}
|
|
387
|
-
return `${it.segment === '*' ? '.*' : escape(it.segment)}\\/`;
|
|
388
|
-
}).join('')})$`) : undefined;
|
|
389
|
-
const segments = parts.map(it => it.segment);
|
|
390
|
-
const params = parts.map((it, i) => it.param ? {
|
|
391
|
-
index: i,
|
|
392
|
-
screen: it.screen,
|
|
393
|
-
name: it.param
|
|
394
|
-
} : null).filter(it => it != null);
|
|
533
|
+
const pattern = segments.join('/');
|
|
395
534
|
return {
|
|
396
535
|
screen,
|
|
536
|
+
order: 0,
|
|
397
537
|
regex,
|
|
538
|
+
pattern,
|
|
398
539
|
segments,
|
|
399
540
|
params,
|
|
400
541
|
routeNames,
|
|
401
|
-
parse
|
|
542
|
+
parse,
|
|
543
|
+
explicitParamNames: getExplicitParamNames(parse),
|
|
544
|
+
pathParamNames,
|
|
545
|
+
hasNestedScreens,
|
|
546
|
+
shared
|
|
402
547
|
};
|
|
403
548
|
};
|
|
404
|
-
const
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
549
|
+
const selectSharedConfig = (match, candidates, previous, initialRoutes) => {
|
|
550
|
+
// Walk the previous state down to the focused leaf to get the active route path
|
|
551
|
+
const focusedRouteNames = [];
|
|
552
|
+
let current = previous;
|
|
553
|
+
while (current?.routes.length) {
|
|
554
|
+
const index = typeof current.index === 'number' ? current.index : current.routes.length - 1;
|
|
555
|
+
const route = current.routes[index];
|
|
556
|
+
if (route == null) {
|
|
557
|
+
break;
|
|
408
558
|
}
|
|
559
|
+
focusedRouteNames.push(route.name);
|
|
560
|
+
current = route.state;
|
|
409
561
|
}
|
|
410
|
-
|
|
562
|
+
|
|
563
|
+
// Rank a candidate by how far it aligns with the focused path, then by
|
|
564
|
+
// how many of its levels match a configured initial route
|
|
565
|
+
const score = routeNames => {
|
|
566
|
+
let focused = 0;
|
|
567
|
+
while (routeNames[focused] !== undefined && routeNames[focused] === focusedRouteNames[focused]) {
|
|
568
|
+
focused++;
|
|
569
|
+
}
|
|
570
|
+
let initial = 0;
|
|
571
|
+
for (const config of initialRoutes) {
|
|
572
|
+
if (arrayStartsWith(routeNames, config.parentScreens) && routeNames[config.parentScreens.length] === config.initialRouteName) {
|
|
573
|
+
initial++;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
return {
|
|
577
|
+
focused,
|
|
578
|
+
initial
|
|
579
|
+
};
|
|
580
|
+
};
|
|
581
|
+
let selected = match;
|
|
582
|
+
let best = {
|
|
583
|
+
focused: -1,
|
|
584
|
+
initial: -1
|
|
585
|
+
};
|
|
586
|
+
for (const candidate of candidates) {
|
|
587
|
+
if (!candidate.shared) {
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
const {
|
|
591
|
+
focused,
|
|
592
|
+
initial
|
|
593
|
+
} = score(candidate.routeNames);
|
|
594
|
+
const better = focused !== best.focused ? focused > best.focused : initial !== best.initial ? initial > best.initial : candidate.order < selected.order;
|
|
595
|
+
if (better) {
|
|
596
|
+
selected = candidate;
|
|
597
|
+
best = {
|
|
598
|
+
focused,
|
|
599
|
+
initial
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
return selected;
|
|
411
604
|
};
|
|
412
605
|
|
|
413
606
|
// Try to find an initial route connected with the one passed
|
|
@@ -416,7 +609,9 @@ const findInitialRoute = (routeName, parentScreens, initialRoutes) => {
|
|
|
416
609
|
if (parentScreens.length === config.parentScreens.length) {
|
|
417
610
|
let sameParents = true;
|
|
418
611
|
for (let i = 0; i < parentScreens.length; i++) {
|
|
419
|
-
|
|
612
|
+
const parentScreen = parentScreens[i];
|
|
613
|
+
const configParentScreen = config.parentScreens[i];
|
|
614
|
+
if (parentScreen == null || configParentScreen == null || parentScreen.localeCompare(configParentScreen) !== 0) {
|
|
420
615
|
sameParents = false;
|
|
421
616
|
break;
|
|
422
617
|
}
|
|
@@ -470,45 +665,120 @@ const createStateObject = (initialRoute, route, isEmpty) => {
|
|
|
470
665
|
}
|
|
471
666
|
}
|
|
472
667
|
};
|
|
473
|
-
const createNestedStateObject = (path, routes, initialRoutes,
|
|
668
|
+
const createNestedStateObject = (path, routes, initialRoutes, routeConfig) => {
|
|
669
|
+
const focusedRoute = routes[routes.length - 1];
|
|
670
|
+
if (focusedRoute == null) {
|
|
671
|
+
return undefined;
|
|
672
|
+
}
|
|
673
|
+
focusedRoute.path = path.replace(/\/$/, '');
|
|
674
|
+
const queryParams = parseQueryParams(path, routeConfig?.parse, routeConfig?.pathParamNames, routeConfig?.explicitParamNames, routeConfig?.hasNestedScreens, focusedRoute.params);
|
|
675
|
+
if (!queryParams.valid) {
|
|
676
|
+
return undefined;
|
|
677
|
+
}
|
|
678
|
+
if (queryParams.params) {
|
|
679
|
+
focusedRoute.params = {
|
|
680
|
+
...focusedRoute.params,
|
|
681
|
+
...queryParams.params
|
|
682
|
+
};
|
|
683
|
+
}
|
|
474
684
|
let route = routes.shift();
|
|
685
|
+
if (route == null) {
|
|
686
|
+
return undefined;
|
|
687
|
+
}
|
|
475
688
|
const parentScreens = [];
|
|
476
689
|
let initialRoute = findInitialRoute(route.name, parentScreens, initialRoutes);
|
|
477
690
|
parentScreens.push(route.name);
|
|
478
691
|
const state = createStateObject(initialRoute, route, routes.length === 0);
|
|
479
692
|
if (routes.length > 0) {
|
|
480
693
|
let nestedState = state;
|
|
481
|
-
|
|
694
|
+
let nextRoute = routes.shift();
|
|
695
|
+
while (nextRoute != null) {
|
|
696
|
+
route = nextRoute;
|
|
482
697
|
initialRoute = findInitialRoute(route.name, parentScreens, initialRoutes);
|
|
483
698
|
const nestedStateIndex = nestedState.index || nestedState.routes.length - 1;
|
|
484
|
-
nestedState.routes[nestedStateIndex]
|
|
699
|
+
const nestedRoute = nestedState.routes[nestedStateIndex];
|
|
700
|
+
if (nestedRoute == null) {
|
|
701
|
+
throw new Error(`Couldn't find a route at index ${nestedStateIndex}.`);
|
|
702
|
+
}
|
|
703
|
+
nestedRoute.state = createStateObject(initialRoute, route, routes.length === 0);
|
|
485
704
|
if (routes.length > 0) {
|
|
486
|
-
|
|
705
|
+
if (nestedRoute.state == null) {
|
|
706
|
+
throw new Error(`Couldn't find nested state for route '${route.name}'.`);
|
|
707
|
+
}
|
|
708
|
+
nestedState = nestedRoute.state;
|
|
487
709
|
}
|
|
488
710
|
parentScreens.push(route.name);
|
|
711
|
+
nextRoute = routes.shift();
|
|
489
712
|
}
|
|
490
713
|
}
|
|
491
|
-
route = findFocusedRoute(state);
|
|
492
|
-
route.path = path.replace(/\/$/, '');
|
|
493
|
-
const params = parseQueryParams(path, flatConfig ? findParseConfigForRoute(route.name, flatConfig) : undefined);
|
|
494
|
-
if (params) {
|
|
495
|
-
route.params = {
|
|
496
|
-
...route.params,
|
|
497
|
-
...params
|
|
498
|
-
};
|
|
499
|
-
}
|
|
500
714
|
return state;
|
|
501
715
|
};
|
|
502
|
-
const parseQueryParams = (path, parseConfig) => {
|
|
503
|
-
const
|
|
504
|
-
const
|
|
716
|
+
const parseQueryParams = (path, parseConfig, pathParamNames = new Set(), explicitParamNames, hasNestedScreens = false, routeParams) => {
|
|
717
|
+
const queryIndex = path.indexOf('?');
|
|
718
|
+
const query = queryIndex === -1 ? undefined : path.slice(queryIndex + 1);
|
|
719
|
+
const params = query ? queryString.parse(query) : {};
|
|
720
|
+
|
|
721
|
+
// Path params should always win over same-named query params.
|
|
722
|
+
for (const name of pathParamNames) {
|
|
723
|
+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
|
724
|
+
delete params[name];
|
|
725
|
+
}
|
|
505
726
|
if (parseConfig) {
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
727
|
+
for (const name in parseConfig) {
|
|
728
|
+
const parser = parseConfig[name];
|
|
729
|
+
if (!parser || pathParamNames.has(name)) {
|
|
730
|
+
continue;
|
|
509
731
|
}
|
|
510
|
-
|
|
732
|
+
const schema = getStandardSchema(parser);
|
|
733
|
+
if (!Object.hasOwn(params, name)) {
|
|
734
|
+
if (!schema) {
|
|
735
|
+
continue;
|
|
736
|
+
}
|
|
737
|
+
const result = getValidationResult(schema, undefined);
|
|
738
|
+
if (result.issues) {
|
|
739
|
+
return {
|
|
740
|
+
valid: false
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
if (result.value !== undefined) {
|
|
744
|
+
params[name] = result.value;
|
|
745
|
+
}
|
|
746
|
+
continue;
|
|
747
|
+
}
|
|
748
|
+
if (schema) {
|
|
749
|
+
const result = getValidationResult(schema, params[name]);
|
|
750
|
+
if (result.issues) {
|
|
751
|
+
return {
|
|
752
|
+
valid: false
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
params[name] = result.value;
|
|
756
|
+
continue;
|
|
757
|
+
}
|
|
758
|
+
const value = Array.isArray(params[name]) ? params[name][0] : params[name];
|
|
759
|
+
if (typeof value !== 'string') {
|
|
760
|
+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
|
761
|
+
delete params[name];
|
|
762
|
+
continue;
|
|
763
|
+
}
|
|
764
|
+
if (typeof parser === 'function') {
|
|
765
|
+
params[name] = parser(value);
|
|
766
|
+
continue;
|
|
767
|
+
}
|
|
768
|
+
throw new Error(INVALID_PARSER_ERROR);
|
|
769
|
+
}
|
|
511
770
|
}
|
|
512
|
-
|
|
771
|
+
if (hasNestedScreens && !explicitParamNames?.has('screen') && (typeof params.screen === 'string' || routeParams != null && 'screen' in routeParams && typeof routeParams.screen === 'string')) {
|
|
772
|
+
for (const name of NESTED_SCREEN_PARAM_NAMES) {
|
|
773
|
+
if (!explicitParamNames?.has(name)) {
|
|
774
|
+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
|
775
|
+
delete params[name];
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
return {
|
|
780
|
+
valid: true,
|
|
781
|
+
params: Object.keys(params).length ? params : undefined
|
|
782
|
+
};
|
|
513
783
|
};
|
|
514
784
|
//# sourceMappingURL=getStateFromPath.js.map
|