@react-navigation/core 7.21.4 → 7.21.6
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 +14 -8
- package/lib/module/BaseNavigationContainer.js.map +1 -1
- package/lib/module/ConsumedParamsContext.js.map +1 -1
- package/lib/module/NavigationIndependentTree.js +8 -4
- package/lib/module/NavigationIndependentTree.js.map +1 -1
- package/lib/module/SceneView.js +16 -39
- package/lib/module/SceneView.js.map +1 -1
- package/lib/module/StaticNavigation.js +1 -1
- package/lib/module/StaticNavigation.js.map +1 -1
- package/lib/module/findFocusedRoute.js +3 -3
- package/lib/module/findFocusedRoute.js.map +1 -1
- package/lib/module/getActionFromState.js.map +1 -1
- package/lib/module/getPathFromState.js +75 -45
- package/lib/module/getPathFromState.js.map +1 -1
- package/lib/module/getPatternParts.js +15 -2
- package/lib/module/getPatternParts.js.map +1 -1
- package/lib/module/getStateFromPath.js +47 -21
- 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/useFocusEvents.js +6 -2
- package/lib/module/useFocusEvents.js.map +1 -1
- package/lib/module/useLazyValue.js.map +1 -1
- package/lib/module/useNavigationBuilder.js +9 -6
- package/lib/module/useNavigationBuilder.js.map +1 -1
- package/lib/module/useNavigationState.js +44 -11
- package/lib/module/useNavigationState.js.map +1 -1
- package/lib/module/useOnPreventRemove.js +1 -3
- package/lib/module/useOnPreventRemove.js.map +1 -1
- package/lib/module/useRouteCache.js +1 -3
- package/lib/module/useRouteCache.js.map +1 -1
- package/lib/module/useSyncState.js +3 -2
- package/lib/module/useSyncState.js.map +1 -1
- package/lib/typescript/src/BaseNavigationContainer.d.ts.map +1 -1
- package/lib/typescript/src/ConsumedParamsContext.d.ts +1 -6
- package/lib/typescript/src/ConsumedParamsContext.d.ts.map +1 -1
- package/lib/typescript/src/EnsureSingleNavigator.d.ts +1 -1
- package/lib/typescript/src/EnsureSingleNavigator.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 +1 -1
- package/lib/typescript/src/NavigationProvider.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 +2 -1
- package/lib/typescript/src/SceneView.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/findFocusedRoute.d.ts.map +1 -1
- package/lib/typescript/src/getPathFromState.d.ts.map +1 -1
- package/lib/typescript/src/getPatternParts.d.ts.map +1 -1
- 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/theming/ThemeProvider.d.ts +1 -1
- package/lib/typescript/src/theming/ThemeProvider.d.ts.map +1 -1
- package/lib/typescript/src/types.d.ts +15 -9
- package/lib/typescript/src/types.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/useFocusEvents.d.ts.map +1 -1
- package/lib/typescript/src/useLazyValue.d.ts.map +1 -1
- package/lib/typescript/src/useNavigationBuilder.d.ts +1 -1
- package/lib/typescript/src/useNavigationBuilder.d.ts.map +1 -1
- package/lib/typescript/src/useNavigationState.d.ts +3 -2
- package/lib/typescript/src/useNavigationState.d.ts.map +1 -1
- package/lib/typescript/src/useOnPreventRemove.d.ts.map +1 -1
- package/lib/typescript/src/useRouteCache.d.ts.map +1 -1
- package/lib/typescript/src/useSyncState.d.ts.map +1 -1
- package/package.json +7 -7
- package/src/BaseNavigationContainer.tsx +16 -10
- package/src/ConsumedParamsContext.tsx +1 -6
- package/src/NavigationIndependentTree.tsx +8 -5
- package/src/SceneView.tsx +18 -51
- package/src/StaticNavigation.tsx +2 -2
- package/src/findFocusedRoute.tsx +5 -3
- package/src/getActionFromState.tsx +3 -1
- package/src/getPathFromState.tsx +130 -59
- package/src/getPatternParts.tsx +15 -2
- package/src/getStateFromPath.tsx +76 -17
- package/src/getStateFromRouteParams.tsx +60 -0
- package/src/types.tsx +42 -24
- package/src/useDescriptors.tsx +1 -1
- package/src/useFocusEvents.tsx +7 -5
- package/src/useLazyValue.tsx +2 -2
- package/src/useNavigationBuilder.tsx +16 -8
- package/src/useNavigationState.tsx +51 -21
- package/src/useOnPreventRemove.tsx +4 -3
- package/src/useRouteCache.tsx +1 -4
- package/src/useSyncState.tsx +3 -2
package/src/getPathFromState.tsx
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
NavigationState,
|
|
3
|
-
PartialState,
|
|
4
|
-
Route,
|
|
5
|
-
} from '@react-navigation/routers';
|
|
1
|
+
import type { NavigationState, PartialState } from '@react-navigation/routers';
|
|
6
2
|
import * as queryString from 'query-string';
|
|
7
3
|
|
|
8
4
|
import { getPatternParts, type PatternPart } from './getPatternParts';
|
|
5
|
+
import { getStateFromRouteParams } from './getStateFromRouteParams';
|
|
9
6
|
import type { PathConfig, PathConfigMap } from './types';
|
|
10
7
|
import { validatePathConfig } from './validatePathConfig';
|
|
11
8
|
|
|
@@ -19,20 +16,40 @@ type State = NavigationState | Omit<PartialState<NavigationState>, 'stale'>;
|
|
|
19
16
|
|
|
20
17
|
type StringifyConfig = Record<string, (value: unknown) => string>;
|
|
21
18
|
|
|
19
|
+
type SerializedParamValue = string | string[] | null;
|
|
20
|
+
|
|
22
21
|
type ConfigItem = {
|
|
23
22
|
parts?: PatternPart[];
|
|
23
|
+
ownParts: PatternPart[];
|
|
24
24
|
stringify?: StringifyConfig;
|
|
25
25
|
screens?: Record<string, ConfigItem>;
|
|
26
26
|
};
|
|
27
27
|
|
|
28
|
-
const
|
|
28
|
+
const serializeParamValue = (value: unknown): SerializedParamValue =>
|
|
29
|
+
value === null
|
|
30
|
+
? null
|
|
31
|
+
: Array.isArray(value)
|
|
32
|
+
? value.map(String)
|
|
33
|
+
: String(value);
|
|
34
|
+
|
|
35
|
+
const getActiveRoute = (
|
|
36
|
+
state: State,
|
|
37
|
+
configs: Record<string, ConfigItem> | undefined,
|
|
38
|
+
getRouteState: (
|
|
39
|
+
route: State['routes'][number],
|
|
40
|
+
config: ConfigItem | undefined
|
|
41
|
+
) => State | undefined
|
|
42
|
+
): { name: string; params?: object } => {
|
|
29
43
|
const route =
|
|
30
44
|
typeof state.index === 'number'
|
|
31
45
|
? state.routes[state.index]
|
|
32
46
|
: state.routes[state.routes.length - 1];
|
|
33
47
|
|
|
34
|
-
|
|
35
|
-
|
|
48
|
+
const config = configs?.[route.name];
|
|
49
|
+
const routeState = getRouteState(route, config);
|
|
50
|
+
|
|
51
|
+
if (routeState) {
|
|
52
|
+
return getActiveRoute(routeState, config?.screens, getRouteState);
|
|
36
53
|
}
|
|
37
54
|
|
|
38
55
|
return route;
|
|
@@ -101,24 +118,55 @@ export function getPathFromState<ParamList extends {}>(
|
|
|
101
118
|
}
|
|
102
119
|
|
|
103
120
|
const configs = getNormalizedConfigs(options);
|
|
121
|
+
const cachedRouteStates = new Map<object, State | undefined>();
|
|
122
|
+
|
|
123
|
+
const getRouteState = (
|
|
124
|
+
route: State['routes'][number],
|
|
125
|
+
config: ConfigItem | undefined
|
|
126
|
+
) => {
|
|
127
|
+
if (route.state) {
|
|
128
|
+
return route.state;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const hasScreenParams =
|
|
132
|
+
route.params &&
|
|
133
|
+
'screen' in route.params &&
|
|
134
|
+
typeof route.params.screen === 'string' &&
|
|
135
|
+
config?.screens?.[route.params.screen];
|
|
136
|
+
|
|
137
|
+
const hasStateParams =
|
|
138
|
+
route.params && 'state' in route.params && config?.screens;
|
|
139
|
+
|
|
140
|
+
if (
|
|
141
|
+
!route.params ||
|
|
142
|
+
!config?.screens ||
|
|
143
|
+
(!hasScreenParams && !hasStateParams)
|
|
144
|
+
) {
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (!cachedRouteStates.has(route)) {
|
|
149
|
+
cachedRouteStates.set(route, getStateFromRouteParams(route.params));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return cachedRouteStates.get(route);
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const focusedRoute = getActiveRoute(state, configs, getRouteState);
|
|
104
156
|
|
|
105
157
|
let path = '/';
|
|
106
158
|
let current: State | undefined = state;
|
|
107
159
|
|
|
108
|
-
const allParams: Record<string, string> = {};
|
|
109
|
-
|
|
110
160
|
while (current) {
|
|
111
161
|
let index = typeof current.index === 'number' ? current.index : 0;
|
|
112
|
-
let route = current.routes[index]
|
|
113
|
-
state?: State;
|
|
114
|
-
};
|
|
162
|
+
let route = current.routes[index];
|
|
115
163
|
|
|
116
164
|
let parts: PatternPart[] | undefined;
|
|
117
165
|
|
|
118
|
-
|
|
119
|
-
let currentOptions = configs;
|
|
166
|
+
const partValues = new Map<PatternPart, SerializedParamValue | undefined>();
|
|
120
167
|
|
|
121
|
-
|
|
168
|
+
let focusedParams: Record<string, SerializedParamValue> | undefined;
|
|
169
|
+
let currentOptions = configs;
|
|
122
170
|
|
|
123
171
|
// Keep all the route names that appeared during going deeper in config in case the pattern is resolved to undefined
|
|
124
172
|
const nestedRouteNames = [];
|
|
@@ -126,19 +174,21 @@ export function getPathFromState<ParamList extends {}>(
|
|
|
126
174
|
let hasNext = true;
|
|
127
175
|
|
|
128
176
|
while (route.name in currentOptions && hasNext) {
|
|
129
|
-
|
|
177
|
+
const config = currentOptions[route.name];
|
|
178
|
+
parts = config.parts;
|
|
179
|
+
const ownParts = config.ownParts;
|
|
130
180
|
|
|
131
181
|
nestedRouteNames.push(route.name);
|
|
132
182
|
|
|
133
183
|
if (route.params) {
|
|
134
|
-
const options =
|
|
184
|
+
const options = config;
|
|
135
185
|
|
|
136
186
|
const currentParams = Object.fromEntries(
|
|
137
187
|
Object.entries(route.params)
|
|
138
|
-
.map(([key, value]): [string,
|
|
188
|
+
.map(([key, value]): [string, SerializedParamValue] | null => {
|
|
139
189
|
if (value === undefined) {
|
|
140
190
|
if (options) {
|
|
141
|
-
const optional =
|
|
191
|
+
const optional = ownParts.find(
|
|
142
192
|
(part) => part.param === key
|
|
143
193
|
)?.optional;
|
|
144
194
|
|
|
@@ -150,51 +200,71 @@ export function getPathFromState<ParamList extends {}>(
|
|
|
150
200
|
}
|
|
151
201
|
}
|
|
152
202
|
|
|
153
|
-
const stringify = options?.stringify?.[key]
|
|
203
|
+
const stringify = options?.stringify?.[key];
|
|
154
204
|
|
|
155
|
-
return [
|
|
205
|
+
return [
|
|
206
|
+
key,
|
|
207
|
+
stringify ? stringify(value) : serializeParamValue(value),
|
|
208
|
+
];
|
|
156
209
|
})
|
|
157
210
|
.filter((entry) => entry != null)
|
|
158
211
|
);
|
|
159
212
|
|
|
160
|
-
|
|
161
|
-
|
|
213
|
+
const claimedParams = new Set<string>();
|
|
214
|
+
|
|
215
|
+
for (const part of ownParts) {
|
|
216
|
+
if (part.param && part.param in currentParams) {
|
|
217
|
+
const value = currentParams[part.param];
|
|
218
|
+
|
|
219
|
+
if (value !== undefined) {
|
|
220
|
+
partValues.set(part, value);
|
|
221
|
+
claimedParams.add(part.param);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
162
224
|
}
|
|
163
225
|
|
|
164
226
|
if (focusedRoute === route) {
|
|
165
227
|
// If this is the focused route, keep the params for later use
|
|
166
228
|
// We save it here since it's been stringified already
|
|
167
|
-
focusedParams = {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
229
|
+
focusedParams = {};
|
|
230
|
+
|
|
231
|
+
for (const key in currentParams) {
|
|
232
|
+
const value = currentParams[key];
|
|
233
|
+
|
|
234
|
+
if (
|
|
235
|
+
!claimedParams.has(key) &&
|
|
236
|
+
value !== undefined &&
|
|
237
|
+
value !== 'undefined'
|
|
238
|
+
) {
|
|
239
|
+
focusedParams[key] = value;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
for (const part of ownParts) {
|
|
246
|
+
if (part.param && !partValues.has(part)) {
|
|
247
|
+
partValues.set(part, undefined);
|
|
180
248
|
}
|
|
181
249
|
}
|
|
182
250
|
|
|
251
|
+
const routeState = getRouteState(route, config);
|
|
252
|
+
|
|
183
253
|
// If there is no `screens` property or no nested state, we return pattern
|
|
184
|
-
if (!
|
|
254
|
+
if (!config.screens || routeState === undefined) {
|
|
185
255
|
hasNext = false;
|
|
186
256
|
} else {
|
|
187
257
|
index =
|
|
188
|
-
typeof
|
|
189
|
-
?
|
|
190
|
-
:
|
|
258
|
+
typeof routeState.index === 'number'
|
|
259
|
+
? routeState.index
|
|
260
|
+
: routeState.routes.length - 1;
|
|
191
261
|
|
|
192
|
-
const nextRoute =
|
|
193
|
-
const nestedConfig =
|
|
262
|
+
const nextRoute = routeState.routes[index];
|
|
263
|
+
const nestedConfig = config.screens;
|
|
194
264
|
|
|
195
265
|
// if there is config for next route name, we go deeper
|
|
196
266
|
if (nestedConfig && nextRoute.name in nestedConfig) {
|
|
197
|
-
route = nextRoute
|
|
267
|
+
route = nextRoute;
|
|
198
268
|
currentOptions = nestedConfig;
|
|
199
269
|
} else {
|
|
200
270
|
// If not, there is no sense in going deeper in config
|
|
@@ -203,9 +273,12 @@ export function getPathFromState<ParamList extends {}>(
|
|
|
203
273
|
}
|
|
204
274
|
}
|
|
205
275
|
|
|
276
|
+
const routeState = getRouteState(route, currentOptions[route.name]);
|
|
277
|
+
|
|
206
278
|
if (currentOptions[route.name] !== undefined) {
|
|
207
279
|
path += parts
|
|
208
|
-
?.map((
|
|
280
|
+
?.map((part) => {
|
|
281
|
+
const { segment, param, optional } = part;
|
|
209
282
|
// We don't know what to show for wildcard patterns
|
|
210
283
|
// Showing the route name seems ok, though whatever we show here will be incorrect
|
|
211
284
|
// Since the page doesn't actually exist
|
|
@@ -215,7 +288,7 @@ export function getPathFromState<ParamList extends {}>(
|
|
|
215
288
|
|
|
216
289
|
// If the path has a pattern for a param, put the param in the path
|
|
217
290
|
if (param) {
|
|
218
|
-
const value =
|
|
291
|
+
const value = partValues.get(part);
|
|
219
292
|
|
|
220
293
|
if (value === undefined && optional) {
|
|
221
294
|
// Optional params without value assigned in route.params should be ignored
|
|
@@ -244,12 +317,12 @@ export function getPathFromState<ParamList extends {}>(
|
|
|
244
317
|
focusedParams = Object.fromEntries(
|
|
245
318
|
Object.entries(focusedRoute.params).map(([key, value]) => [
|
|
246
319
|
key,
|
|
247
|
-
|
|
320
|
+
serializeParamValue(value),
|
|
248
321
|
])
|
|
249
322
|
);
|
|
250
323
|
}
|
|
251
324
|
|
|
252
|
-
if (
|
|
325
|
+
if (routeState) {
|
|
253
326
|
path += '/';
|
|
254
327
|
} else if (focusedParams) {
|
|
255
328
|
for (const param in focusedParams) {
|
|
@@ -266,7 +339,7 @@ export function getPathFromState<ParamList extends {}>(
|
|
|
266
339
|
}
|
|
267
340
|
}
|
|
268
341
|
|
|
269
|
-
current =
|
|
342
|
+
current = routeState;
|
|
270
343
|
}
|
|
271
344
|
|
|
272
345
|
// Include the root path if specified
|
|
@@ -292,14 +365,13 @@ const createConfigItem = (
|
|
|
292
365
|
parentParts?: PatternPart[]
|
|
293
366
|
): ConfigItem => {
|
|
294
367
|
if (typeof config === 'string') {
|
|
295
|
-
|
|
296
|
-
const parts = getPatternParts(config);
|
|
368
|
+
const ownParts = getPatternParts(config);
|
|
297
369
|
|
|
298
370
|
if (parentParts) {
|
|
299
|
-
return { parts: [...parentParts, ...
|
|
371
|
+
return { parts: [...parentParts, ...ownParts], ownParts };
|
|
300
372
|
}
|
|
301
373
|
|
|
302
|
-
return { parts };
|
|
374
|
+
return { parts: ownParts, ownParts };
|
|
303
375
|
}
|
|
304
376
|
|
|
305
377
|
if (config.exact && config.path === undefined) {
|
|
@@ -310,14 +382,12 @@ const createConfigItem = (
|
|
|
310
382
|
|
|
311
383
|
// If an object is specified as the value (e.g. Foo: { ... }),
|
|
312
384
|
// It can have `path` property and `screens` prop which has nested configs
|
|
385
|
+
const ownParts = config.path ? getPatternParts(config.path) : [];
|
|
313
386
|
const parts =
|
|
314
387
|
config.exact !== true
|
|
315
|
-
? [
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
]
|
|
319
|
-
: config.path
|
|
320
|
-
? getPatternParts(config.path)
|
|
388
|
+
? [...(parentParts || []), ...ownParts]
|
|
389
|
+
: ownParts.length
|
|
390
|
+
? ownParts
|
|
321
391
|
: undefined;
|
|
322
392
|
|
|
323
393
|
const screens = config.screens
|
|
@@ -326,6 +396,7 @@ const createConfigItem = (
|
|
|
326
396
|
|
|
327
397
|
return {
|
|
328
398
|
parts,
|
|
399
|
+
ownParts,
|
|
329
400
|
stringify: config.stringify,
|
|
330
401
|
screens,
|
|
331
402
|
};
|
package/src/getPatternParts.tsx
CHANGED
|
@@ -15,6 +15,8 @@ export function getPatternParts(path: string): PatternPart[] {
|
|
|
15
15
|
|
|
16
16
|
let isRegex = false;
|
|
17
17
|
let isParam = false;
|
|
18
|
+
let isEscaped = false;
|
|
19
|
+
let isInCharClass = false;
|
|
18
20
|
let regexInnerParens = 0;
|
|
19
21
|
|
|
20
22
|
// One extra iteration to add the last character
|
|
@@ -34,7 +36,7 @@ export function getPatternParts(path: string): PatternPart[] {
|
|
|
34
36
|
`Encountered ':' in the middle of a segment in path: ${path}`
|
|
35
37
|
);
|
|
36
38
|
}
|
|
37
|
-
} else if (char === '(') {
|
|
39
|
+
} else if (char === '(' && !isEscaped && !isInCharClass) {
|
|
38
40
|
if (isParam) {
|
|
39
41
|
if (isRegex) {
|
|
40
42
|
// The '(' is part of the regex if we're already inside one
|
|
@@ -47,7 +49,7 @@ export function getPatternParts(path: string): PatternPart[] {
|
|
|
47
49
|
`Encountered '(' without preceding ':' in path: ${path}`
|
|
48
50
|
);
|
|
49
51
|
}
|
|
50
|
-
} else if (char === ')') {
|
|
52
|
+
} else if (char === ')' && !isEscaped && !isInCharClass) {
|
|
51
53
|
if (isParam && isRegex) {
|
|
52
54
|
if (regexInnerParens) {
|
|
53
55
|
// The ')' is part of the regex if we're already inside one
|
|
@@ -102,6 +104,17 @@ export function getPatternParts(path: string): PatternPart[] {
|
|
|
102
104
|
if (isRegex) {
|
|
103
105
|
current.regex = current.regex || '';
|
|
104
106
|
current.regex += char;
|
|
107
|
+
|
|
108
|
+
// Track escapes and character classes so parens inside them are not special
|
|
109
|
+
if (isEscaped) {
|
|
110
|
+
isEscaped = false;
|
|
111
|
+
} else if (char === '\\') {
|
|
112
|
+
isEscaped = true;
|
|
113
|
+
} else if (char === '[') {
|
|
114
|
+
isInCharClass = true;
|
|
115
|
+
} else if (char === ']') {
|
|
116
|
+
isInCharClass = false;
|
|
117
|
+
}
|
|
105
118
|
}
|
|
106
119
|
|
|
107
120
|
if (isParam && !isRegex) {
|
package/src/getStateFromPath.tsx
CHANGED
|
@@ -26,7 +26,12 @@ type RouteConfig = {
|
|
|
26
26
|
screen: string;
|
|
27
27
|
regex?: RegExp;
|
|
28
28
|
segments: string[];
|
|
29
|
-
params: {
|
|
29
|
+
params: {
|
|
30
|
+
screen: string;
|
|
31
|
+
name: string;
|
|
32
|
+
index: number;
|
|
33
|
+
regex?: RegExp;
|
|
34
|
+
}[];
|
|
30
35
|
routeNames: string[];
|
|
31
36
|
parse?: ParseConfig;
|
|
32
37
|
explicitParamNames?: Set<string>;
|
|
@@ -53,6 +58,7 @@ type ConfigResources = {
|
|
|
53
58
|
initialRoutes: InitialRouteConfig[];
|
|
54
59
|
configs: RouteConfig[];
|
|
55
60
|
configsByScreen: Record<string, RouteConfig[]>;
|
|
61
|
+
prefixRegex?: RegExp;
|
|
56
62
|
};
|
|
57
63
|
|
|
58
64
|
const NESTED_SCREEN_PARAM_NAMES = [
|
|
@@ -64,6 +70,17 @@ const NESTED_SCREEN_PARAM_NAMES = [
|
|
|
64
70
|
'pop',
|
|
65
71
|
];
|
|
66
72
|
|
|
73
|
+
const getStaticSegmentPattern = (segment: string) =>
|
|
74
|
+
Array.from(segment, (char) => {
|
|
75
|
+
const encoded = encodeURIComponent(char);
|
|
76
|
+
const percentEncoded =
|
|
77
|
+
encoded === char
|
|
78
|
+
? `%${char.charCodeAt(0).toString(16).padStart(2, '0').toUpperCase()}`
|
|
79
|
+
: encoded;
|
|
80
|
+
|
|
81
|
+
return `(?:${escape(char)}|${escape(percentEncoded)})`;
|
|
82
|
+
}).join('');
|
|
83
|
+
|
|
67
84
|
const getExplicitParamNames = (parse?: ParseConfig) => {
|
|
68
85
|
const names = Object.entries(parse ?? {}).map(([name]) => name);
|
|
69
86
|
|
|
@@ -95,7 +112,7 @@ export function getStateFromPath<ParamList extends {}>(
|
|
|
95
112
|
path: string,
|
|
96
113
|
options?: Options<ParamList>
|
|
97
114
|
): ResultState | undefined {
|
|
98
|
-
const { initialRoutes, configs, configsByScreen } =
|
|
115
|
+
const { initialRoutes, configs, configsByScreen, prefixRegex } =
|
|
99
116
|
getConfigResources(options);
|
|
100
117
|
|
|
101
118
|
const screens = options?.screens;
|
|
@@ -103,24 +120,20 @@ export function getStateFromPath<ParamList extends {}>(
|
|
|
103
120
|
let remaining = path
|
|
104
121
|
.replace(/\/+/g, '/') // Replace multiple slash (//) with single ones
|
|
105
122
|
.replace(/^\//, '') // Remove extra leading slash
|
|
106
|
-
.replace(/\?.*$/, '')
|
|
123
|
+
.replace(/\?.*$/, '') // Remove query params which we will handle later
|
|
124
|
+
.replace(/%[0-9a-f]{2}/gi, (match) => match.toUpperCase());
|
|
107
125
|
|
|
108
126
|
// Make sure there is a trailing slash
|
|
109
127
|
remaining = remaining.endsWith('/') ? remaining : `${remaining}/`;
|
|
110
128
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if (prefix) {
|
|
114
|
-
// Make sure there is a trailing slash
|
|
115
|
-
const normalizedPrefix = prefix.endsWith('/') ? prefix : `${prefix}/`;
|
|
129
|
+
if (prefixRegex) {
|
|
130
|
+
const prefixMatch = remaining.match(prefixRegex);
|
|
116
131
|
|
|
117
|
-
|
|
118
|
-
if (!remaining.startsWith(normalizedPrefix)) {
|
|
132
|
+
if (prefixMatch == null) {
|
|
119
133
|
return undefined;
|
|
120
134
|
}
|
|
121
135
|
|
|
122
|
-
|
|
123
|
-
remaining = remaining.replace(normalizedPrefix, '');
|
|
136
|
+
remaining = remaining.slice(prefixMatch[0].length);
|
|
124
137
|
}
|
|
125
138
|
|
|
126
139
|
if (screens === undefined) {
|
|
@@ -219,6 +232,19 @@ function prepareConfigResources(options?: Options<{}>) {
|
|
|
219
232
|
|
|
220
233
|
const initialRoutes = getInitialRoutes(options);
|
|
221
234
|
const configs = getSortedNormalizedConfigs(initialRoutes, options?.screens);
|
|
235
|
+
const prefix = options?.path?.replace(/^\//, '');
|
|
236
|
+
|
|
237
|
+
let prefixRegex: RegExp | undefined;
|
|
238
|
+
|
|
239
|
+
if (prefix) {
|
|
240
|
+
const normalizedPrefix = prefix.endsWith('/') ? prefix : `${prefix}/`;
|
|
241
|
+
const prefixPattern = normalizedPrefix
|
|
242
|
+
.split('/')
|
|
243
|
+
.map(getStaticSegmentPattern)
|
|
244
|
+
.join('/');
|
|
245
|
+
|
|
246
|
+
prefixRegex = new RegExp(`^${prefixPattern}`);
|
|
247
|
+
}
|
|
222
248
|
|
|
223
249
|
const configsByScreen: Record<string, RouteConfig[]> = {};
|
|
224
250
|
const configsByPattern = new Map<string, RouteConfig>();
|
|
@@ -237,6 +263,7 @@ function prepareConfigResources(options?: Options<{}>) {
|
|
|
237
263
|
initialRoutes,
|
|
238
264
|
configs,
|
|
239
265
|
configsByScreen,
|
|
266
|
+
prefixRegex,
|
|
240
267
|
};
|
|
241
268
|
}
|
|
242
269
|
|
|
@@ -264,6 +291,7 @@ function getSortedNormalizedConfigs(
|
|
|
264
291
|
createNormalizedConfigs(key, screens, initialRoutes, [], [], [])
|
|
265
292
|
)
|
|
266
293
|
)
|
|
294
|
+
.map((config, order) => ({ ...config, order }))
|
|
267
295
|
.sort((a, b) => {
|
|
268
296
|
// Sort config from most specific to least specific:
|
|
269
297
|
// - more segments
|
|
@@ -275,7 +303,21 @@ function getSortedNormalizedConfigs(
|
|
|
275
303
|
// If 2 patterns are same, move the one with less route names up
|
|
276
304
|
// This is an error state, so it's only useful for consistent error messages
|
|
277
305
|
if (isArrayEqual(a.segments, b.segments)) {
|
|
278
|
-
|
|
306
|
+
if (
|
|
307
|
+
a.routeNames.length > b.routeNames.length &&
|
|
308
|
+
arrayStartsWith(a.routeNames, b.routeNames)
|
|
309
|
+
) {
|
|
310
|
+
return -1;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (
|
|
314
|
+
b.routeNames.length > a.routeNames.length &&
|
|
315
|
+
arrayStartsWith(b.routeNames, a.routeNames)
|
|
316
|
+
) {
|
|
317
|
+
return 1;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return a.routeNames.length - b.routeNames.length || a.order - b.order;
|
|
279
321
|
}
|
|
280
322
|
|
|
281
323
|
// If one of the patterns starts with the other, it's more exhaustive
|
|
@@ -441,6 +483,15 @@ const matchAgainstConfigs = (
|
|
|
441
483
|
break;
|
|
442
484
|
}
|
|
443
485
|
|
|
486
|
+
if (
|
|
487
|
+
param.regex &&
|
|
488
|
+
value !== decoded &&
|
|
489
|
+
!param.regex.test(decoded)
|
|
490
|
+
) {
|
|
491
|
+
hasInvalidParam = true;
|
|
492
|
+
break;
|
|
493
|
+
}
|
|
494
|
+
|
|
444
495
|
const parser = routeConfig.parse?.[param.name];
|
|
445
496
|
|
|
446
497
|
paramEntries.push([param.name, parser ? parser(decoded) : decoded]);
|
|
@@ -642,12 +693,18 @@ const createConfigItem = (
|
|
|
642
693
|
`^(${parts
|
|
643
694
|
.map((it, i) => {
|
|
644
695
|
if (it.param) {
|
|
645
|
-
const reg = it.regex
|
|
696
|
+
const reg = it.regex
|
|
697
|
+
? `(?:${it.regex})|(?=[^/]*%[0-9A-F]{2})[^/]+`
|
|
698
|
+
: '[^/]+';
|
|
646
699
|
|
|
647
700
|
return `(((?<param_${i}>${reg})\\/)${it.optional ? '?' : ''})`;
|
|
648
701
|
}
|
|
649
702
|
|
|
650
|
-
|
|
703
|
+
if (it.segment === '*') {
|
|
704
|
+
return `.*\\/`;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
return `${getStaticSegmentPattern(it.segment)}\\/`;
|
|
651
708
|
})
|
|
652
709
|
.join('')})$`
|
|
653
710
|
)
|
|
@@ -666,6 +723,7 @@ const createConfigItem = (
|
|
|
666
723
|
index,
|
|
667
724
|
screen: part.screen,
|
|
668
725
|
name: part.param,
|
|
726
|
+
regex: part.regex ? new RegExp(`^(?:${part.regex})$`) : undefined,
|
|
669
727
|
});
|
|
670
728
|
|
|
671
729
|
if (part.screen === screen) {
|
|
@@ -813,8 +871,9 @@ const parseQueryParams = (
|
|
|
813
871
|
hasNestedScreens = false,
|
|
814
872
|
routeParams?: Record<string, unknown>
|
|
815
873
|
) => {
|
|
816
|
-
const
|
|
817
|
-
const
|
|
874
|
+
const queryIndex = path.indexOf('?');
|
|
875
|
+
const query = queryIndex === -1 ? undefined : path.slice(queryIndex + 1);
|
|
876
|
+
const params: Record<string, unknown> = query ? queryString.parse(query) : {};
|
|
818
877
|
|
|
819
878
|
for (const name of pathParamNames) {
|
|
820
879
|
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
|
@@ -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
|
+
}
|