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