@react-navigation/core 8.0.0-alpha.4 → 8.0.0-alpha.5
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.map +1 -1
- package/lib/module/NavigationBuilderContext.js.map +1 -1
- package/lib/module/NavigationIndependentTree.js +8 -4
- package/lib/module/NavigationIndependentTree.js.map +1 -1
- package/lib/module/NavigationProvider.js +14 -3
- package/lib/module/NavigationProvider.js.map +1 -1
- package/lib/module/NavigationStateContext.js.map +1 -1
- package/lib/module/SceneView.js +6 -39
- package/lib/module/SceneView.js.map +1 -1
- package/lib/module/StaticNavigation.js.map +1 -1
- package/lib/module/getPathFromState.js +24 -2
- package/lib/module/getPathFromState.js.map +1 -1
- package/lib/module/getStateFromPath.js +157 -72
- 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.map +1 -1
- package/lib/module/types.js.map +1 -1
- package/lib/module/useIsFocused.js +7 -12
- package/lib/module/useIsFocused.js.map +1 -1
- package/lib/module/useNavigationBuilder.js +71 -28
- package/lib/module/useNavigationBuilder.js.map +1 -1
- package/lib/module/useOnAction.js.map +1 -1
- package/lib/module/useOnRouteFocus.js.map +1 -1
- package/lib/typescript/src/NavigationBuilderContext.d.ts +5 -5
- package/lib/typescript/src/NavigationBuilderContext.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/NavigationIndependentTree.d.ts.map +1 -1
- package/lib/typescript/src/NavigationProvider.d.ts +4 -4
- 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/SceneView.d.ts.map +1 -1
- package/lib/typescript/src/StaticNavigation.d.ts +5 -5
- package/lib/typescript/src/StaticNavigation.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 +3 -3
- package/lib/typescript/src/getActionFromState.d.ts.map +1 -1
- package/lib/typescript/src/getPathFromState.d.ts +2 -2
- package/lib/typescript/src/getPathFromState.d.ts.map +1 -1
- package/lib/typescript/src/getStateFromPath.d.ts +3 -3
- 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 +1 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/types.d.ts +64 -64
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/lib/typescript/src/useDescriptors.d.ts +2 -2
- package/lib/typescript/src/useIsFocused.d.ts +3 -0
- package/lib/typescript/src/useIsFocused.d.ts.map +1 -1
- package/lib/typescript/src/useNavigationBuilder.d.ts +17 -17
- package/lib/typescript/src/useNavigationBuilder.d.ts.map +1 -1
- package/lib/typescript/src/useNavigationHelpers.d.ts +15 -15
- package/lib/typescript/src/useOnAction.d.ts +6 -6
- package/lib/typescript/src/useOnAction.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/useRouteCache.d.ts +2 -2
- package/lib/typescript/src/utilities.d.ts +35 -3
- package/lib/typescript/src/utilities.d.ts.map +1 -1
- package/package.json +10 -8
- package/src/BaseNavigationContainer.tsx +1 -1
- package/src/NavigationBuilderContext.tsx +7 -8
- package/src/NavigationFocusedRouteStateContext.tsx +4 -4
- package/src/NavigationIndependentTree.tsx +8 -5
- package/src/NavigationProvider.tsx +17 -3
- package/src/NavigationStateContext.tsx +5 -6
- package/src/SceneView.tsx +6 -36
- package/src/StaticNavigation.tsx +13 -5
- package/src/findFocusedRoute.tsx +3 -3
- package/src/getActionFromState.tsx +7 -7
- package/src/getPathFromState.tsx +52 -8
- package/src/getStateFromPath.tsx +254 -96
- package/src/getStateFromRouteParams.tsx +60 -0
- package/src/index.tsx +1 -0
- package/src/types.tsx +164 -120
- package/src/useIsFocused.tsx +14 -21
- package/src/useNavigationBuilder.tsx +116 -41
- package/src/useOnAction.tsx +7 -7
- package/src/useOnRouteFocus.tsx +9 -11
- package/src/utilities.tsx +72 -4
|
@@ -10,12 +10,12 @@ import type {
|
|
|
10
10
|
import type { NavigatorScreenParams, PathConfig, PathConfigMap } from './types';
|
|
11
11
|
|
|
12
12
|
type ConfigItem = {
|
|
13
|
-
initialRouteName?: string;
|
|
14
|
-
screens?: Record<string, ConfigItem
|
|
13
|
+
initialRouteName?: string | undefined;
|
|
14
|
+
screens?: Record<string, ConfigItem> | undefined;
|
|
15
15
|
};
|
|
16
16
|
|
|
17
17
|
type Options = {
|
|
18
|
-
initialRouteName?: string;
|
|
18
|
+
initialRouteName?: string | undefined;
|
|
19
19
|
screens: PathConfigMap<object>;
|
|
20
20
|
};
|
|
21
21
|
|
|
@@ -23,8 +23,8 @@ type NavigateAction<State extends NavigationState> = {
|
|
|
23
23
|
type: 'NAVIGATE';
|
|
24
24
|
payload: {
|
|
25
25
|
name: string;
|
|
26
|
-
params?: NavigatorScreenParams<State
|
|
27
|
-
path?: string;
|
|
26
|
+
params?: NavigatorScreenParams<State> | undefined;
|
|
27
|
+
path?: string | undefined;
|
|
28
28
|
};
|
|
29
29
|
};
|
|
30
30
|
|
|
@@ -69,8 +69,8 @@ export function getActionFromState(
|
|
|
69
69
|
| {
|
|
70
70
|
name: string;
|
|
71
71
|
params: NavigatorScreenParams<ParamListBase>;
|
|
72
|
-
path?: string;
|
|
73
|
-
pop?: boolean;
|
|
72
|
+
path?: string | undefined;
|
|
73
|
+
pop?: boolean | undefined;
|
|
74
74
|
}
|
|
75
75
|
| undefined = route
|
|
76
76
|
? { name: route.name, path: route.path, params }
|
package/src/getPathFromState.tsx
CHANGED
|
@@ -6,12 +6,13 @@ import type {
|
|
|
6
6
|
import * as queryString from 'query-string';
|
|
7
7
|
|
|
8
8
|
import { getPatternParts, type PatternPart } from './getPatternParts';
|
|
9
|
+
import { getStateFromRouteParams } from './getStateFromRouteParams';
|
|
9
10
|
import type { PathConfig, PathConfigMap } from './types';
|
|
10
11
|
import { validatePathConfig } from './validatePathConfig';
|
|
11
12
|
|
|
12
13
|
type Options<ParamList extends {}> = {
|
|
13
|
-
path?: string;
|
|
14
|
-
initialRouteName?: string;
|
|
14
|
+
path?: string | undefined;
|
|
15
|
+
initialRouteName?: string | undefined;
|
|
15
16
|
screens: PathConfigMap<ParamList>;
|
|
16
17
|
};
|
|
17
18
|
|
|
@@ -20,12 +21,14 @@ type State = NavigationState | Omit<PartialState<NavigationState>, 'stale'>;
|
|
|
20
21
|
type StringifyConfig = Record<string, ((value: unknown) => string) | undefined>;
|
|
21
22
|
|
|
22
23
|
type ConfigItem = {
|
|
23
|
-
parts?: PatternPart[];
|
|
24
|
-
stringify?: StringifyConfig;
|
|
25
|
-
screens?: Record<string, ConfigItem
|
|
24
|
+
parts?: PatternPart[] | undefined;
|
|
25
|
+
stringify?: StringifyConfig | undefined;
|
|
26
|
+
screens?: Record<string, ConfigItem> | undefined;
|
|
26
27
|
};
|
|
27
28
|
|
|
28
|
-
const getActiveRoute = (
|
|
29
|
+
const getActiveRoute = (
|
|
30
|
+
state: State
|
|
31
|
+
): { name: string; params?: object | undefined } => {
|
|
29
32
|
const route =
|
|
30
33
|
typeof state.index === 'number'
|
|
31
34
|
? state.routes[state.index]
|
|
@@ -57,6 +60,46 @@ const getNormalizedConfigs = (options?: Options<{}>) => {
|
|
|
57
60
|
return normalizedConfigs;
|
|
58
61
|
};
|
|
59
62
|
|
|
63
|
+
const getTransformedState = (
|
|
64
|
+
state: State,
|
|
65
|
+
configs: Record<string, ConfigItem> | undefined
|
|
66
|
+
): Omit<PartialState<NavigationState>, 'stale'> => {
|
|
67
|
+
const routes = state.routes.map(
|
|
68
|
+
(route): Omit<PartialState<NavigationState>, 'stale'>['routes'][number] => {
|
|
69
|
+
if (
|
|
70
|
+
route.state ||
|
|
71
|
+
(configs?.[route.name]?.screens &&
|
|
72
|
+
route.params &&
|
|
73
|
+
(('screen' in route.params &&
|
|
74
|
+
typeof route.params.screen === 'string' &&
|
|
75
|
+
configs[route.name].screens?.[route.params.screen]) ||
|
|
76
|
+
'state' in route.params))
|
|
77
|
+
) {
|
|
78
|
+
const nestedState: State | undefined =
|
|
79
|
+
route.state ?? getStateFromRouteParams(route.params);
|
|
80
|
+
|
|
81
|
+
if (nestedState) {
|
|
82
|
+
return {
|
|
83
|
+
...route,
|
|
84
|
+
state: getTransformedState(
|
|
85
|
+
nestedState,
|
|
86
|
+
configs?.[route.name]?.screens
|
|
87
|
+
),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// @ts-expect-error route.state is handled in previous condition
|
|
93
|
+
return route;
|
|
94
|
+
}
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
...state,
|
|
99
|
+
routes,
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
|
|
60
103
|
/**
|
|
61
104
|
* Utility to serialize a navigation state object to a path string.
|
|
62
105
|
*
|
|
@@ -101,9 +144,10 @@ export function getPathFromState<ParamList extends {}>(
|
|
|
101
144
|
}
|
|
102
145
|
|
|
103
146
|
const configs = getNormalizedConfigs(options);
|
|
147
|
+
const transformedState = getTransformedState(state, configs);
|
|
104
148
|
|
|
105
149
|
let path = '/';
|
|
106
|
-
let current: State | undefined =
|
|
150
|
+
let current: State | undefined = transformedState;
|
|
107
151
|
|
|
108
152
|
const allParams: Record<string, string> = {};
|
|
109
153
|
|
|
@@ -118,7 +162,7 @@ export function getPathFromState<ParamList extends {}>(
|
|
|
118
162
|
let focusedParams: Record<string, string> | undefined;
|
|
119
163
|
let currentOptions = configs;
|
|
120
164
|
|
|
121
|
-
const focusedRoute = getActiveRoute(
|
|
165
|
+
const focusedRoute = getActiveRoute(transformedState);
|
|
122
166
|
|
|
123
167
|
// Keep all the route names that appeared during going deeper in config in case the pattern is resolved to undefined
|
|
124
168
|
const nestedRouteNames = [];
|
package/src/getStateFromPath.tsx
CHANGED
|
@@ -11,23 +11,36 @@ import { findFocusedRoute } from './findFocusedRoute';
|
|
|
11
11
|
import { getPatternParts, type PatternPart } from './getPatternParts';
|
|
12
12
|
import { isArrayEqual } from './isArrayEqual';
|
|
13
13
|
import type { PathConfig, PathConfigMap } from './types';
|
|
14
|
+
import type {
|
|
15
|
+
StandardSchemaV1,
|
|
16
|
+
StandardSchemaValidationResult,
|
|
17
|
+
} from './utilities';
|
|
14
18
|
import { validatePathConfig } from './validatePathConfig';
|
|
15
19
|
|
|
16
20
|
type Options<ParamList extends {}> = {
|
|
17
|
-
path?: string;
|
|
18
|
-
initialRouteName?: string;
|
|
21
|
+
path?: string | undefined;
|
|
22
|
+
initialRouteName?: string | undefined;
|
|
19
23
|
screens: PathConfigMap<ParamList>;
|
|
20
24
|
};
|
|
21
25
|
|
|
22
|
-
type
|
|
26
|
+
type ParseConfigValue =
|
|
27
|
+
| ((value: string) => unknown)
|
|
28
|
+
| StandardSchemaV1<unknown, unknown>;
|
|
29
|
+
|
|
30
|
+
type ParseConfig = Record<string, ParseConfigValue | undefined>;
|
|
31
|
+
|
|
32
|
+
type RouteParseConfig = {
|
|
33
|
+
parseConfig?: ParseConfig | undefined;
|
|
34
|
+
pathParamNames: Set<string>;
|
|
35
|
+
};
|
|
23
36
|
|
|
24
37
|
type RouteConfig = {
|
|
25
38
|
screen: string;
|
|
26
|
-
regex?: RegExp;
|
|
39
|
+
regex?: RegExp | undefined;
|
|
27
40
|
segments: string[];
|
|
28
|
-
params: { screen: string; name?: string; index: number }[];
|
|
41
|
+
params: { screen: string; name?: string | undefined; index: number }[];
|
|
29
42
|
routeNames: string[];
|
|
30
|
-
parse?: ParseConfig;
|
|
43
|
+
parse?: ParseConfig | undefined;
|
|
31
44
|
};
|
|
32
45
|
|
|
33
46
|
type InitialRouteConfig = {
|
|
@@ -36,12 +49,12 @@ type InitialRouteConfig = {
|
|
|
36
49
|
};
|
|
37
50
|
|
|
38
51
|
type ResultState = PartialState<NavigationState> & {
|
|
39
|
-
state?: ResultState;
|
|
52
|
+
state?: ResultState | undefined;
|
|
40
53
|
};
|
|
41
54
|
|
|
42
55
|
type ParsedRoute = {
|
|
43
56
|
name: string;
|
|
44
|
-
path?: string;
|
|
57
|
+
path?: string | undefined;
|
|
45
58
|
params?: Record<string, unknown> | undefined;
|
|
46
59
|
};
|
|
47
60
|
|
|
@@ -50,6 +63,47 @@ type ConfigResources = {
|
|
|
50
63
|
configs: RouteConfig[];
|
|
51
64
|
};
|
|
52
65
|
|
|
66
|
+
const INVALID_SCHEMA_RESULT_ERROR =
|
|
67
|
+
'Invalid validation result from schema. It should be an object with either "value" or "issues" property and cannot be asynchronous.';
|
|
68
|
+
|
|
69
|
+
const INVALID_PARSER_ERROR =
|
|
70
|
+
'Invalid parser. Expected a function or a Standard Schema V1 object.';
|
|
71
|
+
|
|
72
|
+
const PARAM_GROUP_PREFIX = 'param_';
|
|
73
|
+
|
|
74
|
+
const getStandardSchema = (parser: ParseConfigValue) => {
|
|
75
|
+
if (
|
|
76
|
+
'~standard' in parser &&
|
|
77
|
+
typeof parser['~standard'] === 'object' &&
|
|
78
|
+
parser['~standard'] !== null &&
|
|
79
|
+
'version' in parser['~standard'] &&
|
|
80
|
+
parser['~standard'].version === 1 &&
|
|
81
|
+
'validate' in parser['~standard'] &&
|
|
82
|
+
typeof parser['~standard'].validate === 'function'
|
|
83
|
+
) {
|
|
84
|
+
return parser['~standard'];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return undefined;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const getValidationResult = (
|
|
91
|
+
schema: StandardSchemaV1<unknown, unknown>['~standard'],
|
|
92
|
+
value: unknown
|
|
93
|
+
): StandardSchemaValidationResult<unknown> => {
|
|
94
|
+
const result = schema.validate(value);
|
|
95
|
+
|
|
96
|
+
if (
|
|
97
|
+
result != null &&
|
|
98
|
+
typeof result === 'object' &&
|
|
99
|
+
('value' in result || ('issues' in result && Array.isArray(result.issues)))
|
|
100
|
+
) {
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
throw new Error(INVALID_SCHEMA_RESULT_ERROR);
|
|
105
|
+
};
|
|
106
|
+
|
|
53
107
|
/**
|
|
54
108
|
* Utility to parse a path string to initial state object accepted by the container.
|
|
55
109
|
* This is useful for deep linking when we need to handle the incoming URL.
|
|
@@ -136,25 +190,23 @@ export function getStateFromPath<ParamList extends {}>(
|
|
|
136
190
|
return undefined;
|
|
137
191
|
}
|
|
138
192
|
|
|
139
|
-
let result: PartialState<NavigationState> | undefined;
|
|
140
|
-
let current: PartialState<NavigationState> | undefined;
|
|
141
|
-
|
|
142
193
|
// We match the whole path against the regex instead of segments
|
|
143
194
|
// This makes sure matches such as wildcard will catch any unmatched routes, even if nested
|
|
144
|
-
const
|
|
195
|
+
for (const config of configs) {
|
|
196
|
+
const routes = matchAgainstConfig(remaining, config, configs);
|
|
145
197
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
remaining = remainingPath;
|
|
150
|
-
result = current;
|
|
151
|
-
}
|
|
198
|
+
if (routes === undefined) {
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
152
201
|
|
|
153
|
-
|
|
154
|
-
|
|
202
|
+
const state = createNestedStateObject(path, routes, initialRoutes, configs);
|
|
203
|
+
|
|
204
|
+
if (state !== undefined) {
|
|
205
|
+
return state;
|
|
206
|
+
}
|
|
155
207
|
}
|
|
156
208
|
|
|
157
|
-
return
|
|
209
|
+
return undefined;
|
|
158
210
|
}
|
|
159
211
|
|
|
160
212
|
/**
|
|
@@ -344,75 +396,101 @@ function getConfigsWithRegexes(configs: RouteConfig[]) {
|
|
|
344
396
|
}));
|
|
345
397
|
}
|
|
346
398
|
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
|
|
399
|
+
const matchAgainstConfig = (
|
|
400
|
+
remaining: string,
|
|
401
|
+
config: RouteConfig,
|
|
402
|
+
configs: RouteConfig[]
|
|
403
|
+
) => {
|
|
404
|
+
if (!config.regex) {
|
|
405
|
+
return undefined;
|
|
406
|
+
}
|
|
350
407
|
|
|
351
|
-
|
|
352
|
-
for (const config of configs) {
|
|
353
|
-
if (!config.regex) {
|
|
354
|
-
continue;
|
|
355
|
-
}
|
|
408
|
+
const match = remaining.match(config.regex);
|
|
356
409
|
|
|
357
|
-
|
|
410
|
+
if (!match) {
|
|
411
|
+
return undefined;
|
|
412
|
+
}
|
|
358
413
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
routes = config.routeNames.map((routeName) => {
|
|
362
|
-
const routeConfig = configs.find((c) => {
|
|
363
|
-
// Check matching name AND pattern in case same screen is used at different levels in config
|
|
364
|
-
return (
|
|
365
|
-
c.screen === routeName &&
|
|
366
|
-
arrayStartsWith(config.segments, c.segments)
|
|
367
|
-
);
|
|
368
|
-
});
|
|
414
|
+
let validationFailed = false;
|
|
415
|
+
const matchedRoutes: ParsedRoute[] = [];
|
|
369
416
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
const param = routeConfig.params.find(
|
|
377
|
-
(it) => it.index === index
|
|
378
|
-
);
|
|
379
|
-
|
|
380
|
-
if (param?.screen === routeName && param?.name) {
|
|
381
|
-
return [param.name, value];
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
return null;
|
|
385
|
-
})
|
|
386
|
-
.filter((it) => it != null)
|
|
387
|
-
.map(([key, value]) => {
|
|
388
|
-
if (value == null) {
|
|
389
|
-
return [key, undefined];
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
const decoded = decodeURIComponent(value);
|
|
393
|
-
const parsed = routeConfig.parse?.[key]
|
|
394
|
-
? routeConfig.parse[key](decoded)
|
|
395
|
-
: decoded;
|
|
396
|
-
|
|
397
|
-
return [key, parsed];
|
|
398
|
-
})
|
|
399
|
-
)
|
|
400
|
-
: undefined;
|
|
417
|
+
for (const routeName of config.routeNames) {
|
|
418
|
+
// Check matching name AND pattern in case same screen is used at different levels in config
|
|
419
|
+
const routeConfig = configs.find(
|
|
420
|
+
(c) =>
|
|
421
|
+
c.screen === routeName && arrayStartsWith(config.segments, c.segments)
|
|
422
|
+
);
|
|
401
423
|
|
|
402
|
-
|
|
403
|
-
|
|
424
|
+
let params: Record<string, unknown> | undefined;
|
|
425
|
+
|
|
426
|
+
if (routeConfig && match.groups) {
|
|
427
|
+
const paramEntries: [string, unknown][] = [];
|
|
428
|
+
|
|
429
|
+
for (const [key, value] of Object.entries(match.groups)) {
|
|
430
|
+
const index = Number(key.replace(PARAM_GROUP_PREFIX, ''));
|
|
431
|
+
const param = routeConfig.params.find((it) => it.index === index);
|
|
432
|
+
|
|
433
|
+
if (param?.screen !== routeName || !param.name) {
|
|
434
|
+
continue;
|
|
404
435
|
}
|
|
405
436
|
|
|
406
|
-
|
|
407
|
-
|
|
437
|
+
if (value == null) {
|
|
438
|
+
paramEntries.push([param.name, undefined]);
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const decoded = decodeURIComponent(value);
|
|
443
|
+
const parser = routeConfig.parse?.[param.name];
|
|
408
444
|
|
|
409
|
-
|
|
445
|
+
if (!parser) {
|
|
446
|
+
paramEntries.push([param.name, decoded]);
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
410
449
|
|
|
411
|
-
|
|
450
|
+
const schema = getStandardSchema(parser);
|
|
451
|
+
|
|
452
|
+
if (schema) {
|
|
453
|
+
const result = getValidationResult(schema, decoded);
|
|
454
|
+
|
|
455
|
+
if (result.issues) {
|
|
456
|
+
validationFailed = true;
|
|
457
|
+
break;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
paramEntries.push([param.name, result.value]);
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (typeof parser === 'function') {
|
|
465
|
+
paramEntries.push([param.name, parser(decoded)]);
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
throw new Error(INVALID_PARSER_ERROR);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (validationFailed) {
|
|
473
|
+
// A failed param validation invalidates the whole nested route chain for this config.
|
|
474
|
+
break;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
if (paramEntries.length) {
|
|
478
|
+
params = Object.fromEntries(paramEntries);
|
|
479
|
+
}
|
|
412
480
|
}
|
|
481
|
+
|
|
482
|
+
if (params && Object.keys(params).length) {
|
|
483
|
+
matchedRoutes.push({ name: routeName, params });
|
|
484
|
+
} else {
|
|
485
|
+
matchedRoutes.push({ name: routeName });
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
if (validationFailed) {
|
|
490
|
+
return undefined;
|
|
413
491
|
}
|
|
414
492
|
|
|
415
|
-
return
|
|
493
|
+
return matchedRoutes;
|
|
416
494
|
};
|
|
417
495
|
|
|
418
496
|
const createNormalizedConfigs = (
|
|
@@ -551,7 +629,7 @@ const createConfigItem = (
|
|
|
551
629
|
if (it.param) {
|
|
552
630
|
const reg = it.regex || '[^/]+';
|
|
553
631
|
|
|
554
|
-
return `(((
|
|
632
|
+
return `(((?<${PARAM_GROUP_PREFIX}${i}>${reg})\\/)${it.optional ? '?' : ''})`;
|
|
555
633
|
}
|
|
556
634
|
|
|
557
635
|
return `${it.segment === '*' ? '.*' : escape(it.segment)}\\/`;
|
|
@@ -586,10 +664,22 @@ const createConfigItem = (
|
|
|
586
664
|
const findParseConfigForRoute = (
|
|
587
665
|
routeName: string,
|
|
588
666
|
flatConfig: RouteConfig[]
|
|
589
|
-
):
|
|
667
|
+
): RouteParseConfig | undefined => {
|
|
590
668
|
for (const config of flatConfig) {
|
|
591
669
|
if (routeName === config.routeNames[config.routeNames.length - 1]) {
|
|
592
|
-
return
|
|
670
|
+
return {
|
|
671
|
+
parseConfig: config.parse,
|
|
672
|
+
pathParamNames: new Set(
|
|
673
|
+
config.params
|
|
674
|
+
.filter(
|
|
675
|
+
(
|
|
676
|
+
param
|
|
677
|
+
): param is { screen: string; name: string; index: number } =>
|
|
678
|
+
param.screen === routeName && typeof param.name === 'string'
|
|
679
|
+
)
|
|
680
|
+
.map((param) => param.name)
|
|
681
|
+
),
|
|
682
|
+
};
|
|
593
683
|
}
|
|
594
684
|
}
|
|
595
685
|
|
|
@@ -658,7 +748,7 @@ const createNestedStateObject = (
|
|
|
658
748
|
routes: ParsedRoute[],
|
|
659
749
|
initialRoutes: InitialRouteConfig[],
|
|
660
750
|
flatConfig?: RouteConfig[]
|
|
661
|
-
) => {
|
|
751
|
+
): InitialState | undefined => {
|
|
662
752
|
let route = routes.shift() as ParsedRoute;
|
|
663
753
|
const parentScreens: string[] = [];
|
|
664
754
|
|
|
@@ -699,33 +789,101 @@ const createNestedStateObject = (
|
|
|
699
789
|
route = findFocusedRoute(state) as ParsedRoute;
|
|
700
790
|
route.path = path.replace(/\/$/, '');
|
|
701
791
|
|
|
702
|
-
const
|
|
792
|
+
const parseConfigForRoute = flatConfig
|
|
793
|
+
? findParseConfigForRoute(route.name, flatConfig)
|
|
794
|
+
: undefined;
|
|
795
|
+
|
|
796
|
+
const queryParams = parseQueryParams(
|
|
703
797
|
path,
|
|
704
|
-
|
|
798
|
+
parseConfigForRoute?.parseConfig,
|
|
799
|
+
parseConfigForRoute?.pathParamNames
|
|
705
800
|
);
|
|
706
801
|
|
|
707
|
-
if (
|
|
708
|
-
|
|
802
|
+
if (!queryParams.valid) {
|
|
803
|
+
return undefined;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
if (queryParams.params) {
|
|
807
|
+
route.params = { ...route.params, ...queryParams.params };
|
|
709
808
|
}
|
|
710
809
|
|
|
711
810
|
return state;
|
|
712
811
|
};
|
|
713
812
|
|
|
714
|
-
const parseQueryParams = (
|
|
813
|
+
const parseQueryParams = (
|
|
814
|
+
path: string,
|
|
815
|
+
parseConfig?: ParseConfig,
|
|
816
|
+
pathParamNames: Set<string> = new Set()
|
|
817
|
+
):
|
|
818
|
+
| { valid: true; params?: Record<string, unknown> | undefined }
|
|
819
|
+
| { valid: false } => {
|
|
715
820
|
const query = path.split('?')[1];
|
|
716
821
|
const params: Record<string, unknown> = queryString.parse(query);
|
|
717
822
|
|
|
823
|
+
// Path params should always win over same-named query params.
|
|
824
|
+
for (const name of pathParamNames) {
|
|
825
|
+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
|
826
|
+
delete params[name];
|
|
827
|
+
}
|
|
828
|
+
|
|
718
829
|
if (parseConfig) {
|
|
719
|
-
Object.
|
|
720
|
-
if (
|
|
721
|
-
|
|
722
|
-
parseConfig[name] &&
|
|
723
|
-
typeof params[name] === 'string'
|
|
724
|
-
) {
|
|
725
|
-
params[name] = parseConfig[name](params[name]);
|
|
830
|
+
for (const [name, parser] of Object.entries(parseConfig)) {
|
|
831
|
+
if (!parser || pathParamNames.has(name)) {
|
|
832
|
+
continue;
|
|
726
833
|
}
|
|
727
|
-
|
|
834
|
+
|
|
835
|
+
const schema = getStandardSchema(parser);
|
|
836
|
+
|
|
837
|
+
if (!Object.hasOwn(params, name)) {
|
|
838
|
+
if (!schema) {
|
|
839
|
+
continue;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
const result = getValidationResult(schema, undefined);
|
|
843
|
+
|
|
844
|
+
if (result.issues) {
|
|
845
|
+
return { valid: false };
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
if (result.value !== undefined) {
|
|
849
|
+
params[name] = result.value;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
continue;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
if (schema) {
|
|
856
|
+
const result = getValidationResult(schema, params[name]);
|
|
857
|
+
|
|
858
|
+
if (result.issues) {
|
|
859
|
+
return { valid: false };
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
params[name] = result.value;
|
|
863
|
+
continue;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
const value = Array.isArray(params[name])
|
|
867
|
+
? params[name][0]
|
|
868
|
+
: params[name];
|
|
869
|
+
|
|
870
|
+
if (typeof value !== 'string') {
|
|
871
|
+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
|
872
|
+
delete params[name];
|
|
873
|
+
continue;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
if (typeof parser === 'function') {
|
|
877
|
+
params[name] = parser(value);
|
|
878
|
+
continue;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
throw new Error(INVALID_PARSER_ERROR);
|
|
882
|
+
}
|
|
728
883
|
}
|
|
729
884
|
|
|
730
|
-
return
|
|
885
|
+
return {
|
|
886
|
+
valid: true,
|
|
887
|
+
params: Object.keys(params).length ? params : undefined,
|
|
888
|
+
};
|
|
731
889
|
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { NavigationState, PartialState } from '@react-navigation/routers';
|
|
2
|
+
|
|
3
|
+
export function getStateFromRouteParams(
|
|
4
|
+
params: object | undefined
|
|
5
|
+
): PartialState<NavigationState> | NavigationState | undefined {
|
|
6
|
+
if (params == null || typeof params !== 'object') {
|
|
7
|
+
return undefined;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if (
|
|
11
|
+
'state' in params &&
|
|
12
|
+
params.state &&
|
|
13
|
+
typeof params.state === 'object' &&
|
|
14
|
+
'routes' in params.state &&
|
|
15
|
+
Array.isArray(params.state.routes) &&
|
|
16
|
+
params.state.routes.every(
|
|
17
|
+
(route) =>
|
|
18
|
+
typeof route === 'object' &&
|
|
19
|
+
route != null &&
|
|
20
|
+
'name' in route &&
|
|
21
|
+
typeof route.name === 'string'
|
|
22
|
+
)
|
|
23
|
+
) {
|
|
24
|
+
// @ts-expect-error this is fine 🔥
|
|
25
|
+
return params.state;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (
|
|
29
|
+
'screen' in params &&
|
|
30
|
+
params.screen &&
|
|
31
|
+
typeof params.screen === 'string'
|
|
32
|
+
) {
|
|
33
|
+
return {
|
|
34
|
+
routes: [
|
|
35
|
+
{
|
|
36
|
+
name: params.screen,
|
|
37
|
+
params:
|
|
38
|
+
'params' in params &&
|
|
39
|
+
typeof params.params === 'object' &&
|
|
40
|
+
params.params != null
|
|
41
|
+
? params.params
|
|
42
|
+
: undefined,
|
|
43
|
+
path:
|
|
44
|
+
'path' in params && typeof params.path === 'string'
|
|
45
|
+
? params.path
|
|
46
|
+
: undefined,
|
|
47
|
+
// @ts-expect-error this is fine 🔥
|
|
48
|
+
state:
|
|
49
|
+
'params' in params &&
|
|
50
|
+
typeof params.params === 'object' &&
|
|
51
|
+
params.params != null
|
|
52
|
+
? getStateFromRouteParams(params.params)
|
|
53
|
+
: undefined,
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
package/src/index.tsx
CHANGED
|
@@ -44,5 +44,6 @@ export { usePreventRemove } from './usePreventRemove';
|
|
|
44
44
|
export { usePreventRemoveContext } from './usePreventRemoveContext';
|
|
45
45
|
export { useRoute } from './useRoute';
|
|
46
46
|
export { useStateForPath } from './useStateForPath';
|
|
47
|
+
export type { QueryParamInput, StandardSchemaV1 } from './utilities';
|
|
47
48
|
export { validatePathConfig } from './validatePathConfig';
|
|
48
49
|
export * from '@react-navigation/routers';
|