@react-navigation/core 8.0.0-alpha.23 → 8.0.0-alpha.25

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.
Files changed (153) hide show
  1. package/lib/module/BaseNavigationContainer.js +5 -2
  2. package/lib/module/BaseNavigationContainer.js.map +1 -1
  3. package/lib/module/DataLoading.js +102 -0
  4. package/lib/module/DataLoading.js.map +1 -0
  5. package/lib/module/NavigationBuilderContext.js.map +1 -1
  6. package/lib/module/NavigationIndependentTree.js +7 -6
  7. package/lib/module/NavigationIndependentTree.js.map +1 -1
  8. package/lib/module/NavigationProvider.js +12 -12
  9. package/lib/module/NavigationProvider.js.map +1 -1
  10. package/lib/module/PreventRemoveContext.js.map +1 -1
  11. package/lib/module/PreventRemoveProvider.js +34 -24
  12. package/lib/module/PreventRemoveProvider.js.map +1 -1
  13. package/lib/module/StaticNavigation.js +15 -7
  14. package/lib/module/StaticNavigation.js.map +1 -1
  15. package/lib/module/StaticTreeContext.js +5 -0
  16. package/lib/module/StaticTreeContext.js.map +1 -0
  17. package/lib/module/checkSerializable.js +7 -9
  18. package/lib/module/checkSerializable.js.map +1 -1
  19. package/lib/module/createNavigationContainerRef.js +6 -8
  20. package/lib/module/createNavigationContainerRef.js.map +1 -1
  21. package/lib/module/createNavigatorFactory.js.map +1 -1
  22. package/lib/module/getActionFromState.js +11 -11
  23. package/lib/module/getActionFromState.js.map +1 -1
  24. package/lib/module/getPathFromState.js +125 -96
  25. package/lib/module/getPathFromState.js.map +1 -1
  26. package/lib/module/getPatternParts.js +2 -2
  27. package/lib/module/getPatternParts.js.map +1 -1
  28. package/lib/module/getStateFromPath.js +136 -78
  29. package/lib/module/getStateFromPath.js.map +1 -1
  30. package/lib/module/index.js +1 -0
  31. package/lib/module/index.js.map +1 -1
  32. package/lib/module/types.js +13 -0
  33. package/lib/module/types.js.map +1 -1
  34. package/lib/module/useEventEmitter.js +5 -2
  35. package/lib/module/useEventEmitter.js.map +1 -1
  36. package/lib/module/useKeyedChildListeners.js +4 -2
  37. package/lib/module/useKeyedChildListeners.js.map +1 -1
  38. package/lib/module/useNavigation.js +2 -15
  39. package/lib/module/useNavigation.js.map +1 -1
  40. package/lib/module/useNavigationBuilder.js +5 -21
  41. package/lib/module/useNavigationBuilder.js.map +1 -1
  42. package/lib/module/useNavigationCache.js +19 -14
  43. package/lib/module/useNavigationCache.js.map +1 -1
  44. package/lib/module/useNavigationHelpers.js +27 -9
  45. package/lib/module/useNavigationHelpers.js.map +1 -1
  46. package/lib/module/useNavigationState.js +69 -36
  47. package/lib/module/useNavigationState.js.map +1 -1
  48. package/lib/module/useOnAction.js +25 -3
  49. package/lib/module/useOnAction.js.map +1 -1
  50. package/lib/module/useOnPreventRemove.js +35 -24
  51. package/lib/module/useOnPreventRemove.js.map +1 -1
  52. package/lib/module/useOptionsGetters.js +3 -1
  53. package/lib/module/useOptionsGetters.js.map +1 -1
  54. package/lib/module/usePreventRemove.js +9 -2
  55. package/lib/module/usePreventRemove.js.map +1 -1
  56. package/lib/module/useRoute.js.map +1 -1
  57. package/lib/module/useRouteCache.js +14 -7
  58. package/lib/module/useRouteCache.js.map +1 -1
  59. package/lib/module/useScheduleUpdate.js +8 -8
  60. package/lib/module/useScheduleUpdate.js.map +1 -1
  61. package/lib/module/useSyncState.js +4 -1
  62. package/lib/module/useSyncState.js.map +1 -1
  63. package/lib/module/validatePathConfig.js +50 -37
  64. package/lib/module/validatePathConfig.js.map +1 -1
  65. package/lib/typescript/src/BaseNavigationContainer.d.ts.map +1 -1
  66. package/lib/typescript/src/DataLoading.d.ts +20 -0
  67. package/lib/typescript/src/DataLoading.d.ts.map +1 -0
  68. package/lib/typescript/src/NavigationBuilderContext.d.ts +2 -2
  69. package/lib/typescript/src/NavigationBuilderContext.d.ts.map +1 -1
  70. package/lib/typescript/src/NavigationProvider.d.ts +0 -1
  71. package/lib/typescript/src/NavigationProvider.d.ts.map +1 -1
  72. package/lib/typescript/src/PreventRemoveContext.d.ts +1 -0
  73. package/lib/typescript/src/PreventRemoveContext.d.ts.map +1 -1
  74. package/lib/typescript/src/PreventRemoveProvider.d.ts.map +1 -1
  75. package/lib/typescript/src/StaticNavigation.d.ts +68 -42
  76. package/lib/typescript/src/StaticNavigation.d.ts.map +1 -1
  77. package/lib/typescript/src/StaticTreeContext.d.ts +4 -0
  78. package/lib/typescript/src/StaticTreeContext.d.ts.map +1 -0
  79. package/lib/typescript/src/checkSerializable.d.ts +4 -2
  80. package/lib/typescript/src/checkSerializable.d.ts.map +1 -1
  81. package/lib/typescript/src/createNavigationContainerRef.d.ts.map +1 -1
  82. package/lib/typescript/src/createNavigatorFactory.d.ts +11 -5
  83. package/lib/typescript/src/createNavigatorFactory.d.ts.map +1 -1
  84. package/lib/typescript/src/getActionFromState.d.ts.map +1 -1
  85. package/lib/typescript/src/getPathFromState.d.ts.map +1 -1
  86. package/lib/typescript/src/getStateFromPath.d.ts.map +1 -1
  87. package/lib/typescript/src/index.d.ts +2 -1
  88. package/lib/typescript/src/index.d.ts.map +1 -1
  89. package/lib/typescript/src/types.d.ts +84 -29
  90. package/lib/typescript/src/types.d.ts.map +1 -1
  91. package/lib/typescript/src/useEventEmitter.d.ts +1 -1
  92. package/lib/typescript/src/useEventEmitter.d.ts.map +1 -1
  93. package/lib/typescript/src/useKeyedChildListeners.d.ts.map +1 -1
  94. package/lib/typescript/src/useNavigation.d.ts.map +1 -1
  95. package/lib/typescript/src/useNavigationBuilder.d.ts +1 -1
  96. package/lib/typescript/src/useNavigationBuilder.d.ts.map +1 -1
  97. package/lib/typescript/src/useNavigationCache.d.ts.map +1 -1
  98. package/lib/typescript/src/useNavigationHelpers.d.ts +3 -4
  99. package/lib/typescript/src/useNavigationHelpers.d.ts.map +1 -1
  100. package/lib/typescript/src/useNavigationState.d.ts +11 -1
  101. package/lib/typescript/src/useNavigationState.d.ts.map +1 -1
  102. package/lib/typescript/src/useOnAction.d.ts.map +1 -1
  103. package/lib/typescript/src/useOnPreventRemove.d.ts +2 -6
  104. package/lib/typescript/src/useOnPreventRemove.d.ts.map +1 -1
  105. package/lib/typescript/src/useOptionsGetters.d.ts.map +1 -1
  106. package/lib/typescript/src/usePreventRemove.d.ts.map +1 -1
  107. package/lib/typescript/src/usePreventRemoveContext.d.ts +1 -0
  108. package/lib/typescript/src/usePreventRemoveContext.d.ts.map +1 -1
  109. package/lib/typescript/src/useRoute.d.ts +1 -1
  110. package/lib/typescript/src/useRoute.d.ts.map +1 -1
  111. package/lib/typescript/src/useRouteCache.d.ts.map +1 -1
  112. package/lib/typescript/src/useScheduleUpdate.d.ts +5 -4
  113. package/lib/typescript/src/useScheduleUpdate.d.ts.map +1 -1
  114. package/lib/typescript/src/useSyncState.d.ts.map +1 -1
  115. package/lib/typescript/src/utilities.d.ts +21 -38
  116. package/lib/typescript/src/utilities.d.ts.map +1 -1
  117. package/lib/typescript/src/validatePathConfig.d.ts.map +1 -1
  118. package/package.json +6 -8
  119. package/src/BaseNavigationContainer.tsx +9 -2
  120. package/src/DataLoading.tsx +139 -0
  121. package/src/NavigationBuilderContext.tsx +5 -1
  122. package/src/NavigationIndependentTree.tsx +11 -11
  123. package/src/NavigationProvider.tsx +13 -16
  124. package/src/PreventRemoveContext.tsx +1 -0
  125. package/src/PreventRemoveProvider.tsx +48 -43
  126. package/src/StaticNavigation.tsx +164 -99
  127. package/src/StaticTreeContext.tsx +7 -0
  128. package/src/checkSerializable.tsx +20 -14
  129. package/src/createNavigationContainerRef.tsx +12 -9
  130. package/src/createNavigatorFactory.tsx +27 -18
  131. package/src/getActionFromState.tsx +18 -16
  132. package/src/getPathFromState.tsx +167 -132
  133. package/src/getPatternParts.tsx +2 -2
  134. package/src/getStateFromPath.tsx +260 -193
  135. package/src/index.tsx +3 -0
  136. package/src/types.tsx +298 -62
  137. package/src/useEventEmitter.tsx +7 -2
  138. package/src/useKeyedChildListeners.tsx +4 -2
  139. package/src/useNavigation.tsx +2 -21
  140. package/src/useNavigationBuilder.tsx +7 -24
  141. package/src/useNavigationCache.tsx +26 -19
  142. package/src/useNavigationHelpers.tsx +26 -9
  143. package/src/useNavigationState.tsx +90 -60
  144. package/src/useOnAction.tsx +55 -3
  145. package/src/useOnPreventRemove.tsx +68 -36
  146. package/src/useOptionsGetters.tsx +4 -1
  147. package/src/usePreventRemove.tsx +11 -2
  148. package/src/useRoute.tsx +3 -5
  149. package/src/useRouteCache.tsx +19 -11
  150. package/src/useScheduleUpdate.tsx +8 -7
  151. package/src/useSyncState.tsx +5 -1
  152. package/src/utilities.tsx +36 -41
  153. package/src/validatePathConfig.tsx +86 -58
@@ -37,11 +37,13 @@ type RouteConfig = {
37
37
  screen: string;
38
38
  order: number;
39
39
  regex?: RegExp | undefined;
40
+ pattern: string;
40
41
  segments: string[];
41
- params: { screen: string; name?: string | undefined; index: number }[];
42
+ params: { screen: string; name: string; index: number }[];
42
43
  routeNames: string[];
43
44
  parse?: ParseConfig | undefined;
44
45
  explicitParamNames?: Set<string> | undefined;
46
+ pathParamNames: Set<string>;
45
47
  hasNestedScreens: boolean;
46
48
  shared: boolean;
47
49
  };
@@ -57,7 +59,6 @@ type ResultState = PartialState<NavigationState> & {
57
59
 
58
60
  type ParsedRoute = {
59
61
  name: string;
60
- path?: string | undefined;
61
62
  params?: Record<string, unknown> | undefined;
62
63
  };
63
64
 
@@ -88,9 +89,15 @@ const NESTED_SCREEN_PARAM_NAMES = [
88
89
  ];
89
90
 
90
91
  const getExplicitParamNames = (parse?: ParseConfig) => {
91
- const names = Object.entries(parse ?? {})
92
- .filter(([, parser]) => parser != null)
93
- .map(([name]) => name);
92
+ const names: string[] = [];
93
+
94
+ if (parse) {
95
+ for (const name in parse) {
96
+ if (parse[name] != null) {
97
+ names.push(name);
98
+ }
99
+ }
100
+ }
94
101
 
95
102
  return names.length ? new Set(names) : undefined;
96
103
  };
@@ -163,7 +170,7 @@ export function getStateFromPath<ParamList extends {}>(
163
170
  // pick the one that best matches the current navigation state
164
171
  const resolveSharedConfig = (config: RouteConfig) => {
165
172
  if (config.shared) {
166
- const candidates = configsByPattern[config.segments.join('/')];
173
+ const candidates = configsByPattern[config.pattern];
167
174
 
168
175
  if (candidates) {
169
176
  return selectSharedConfig(config, candidates, previous, initialRoutes);
@@ -196,23 +203,21 @@ export function getStateFromPath<ParamList extends {}>(
196
203
  remaining = remaining.replace(normalizedPrefix, '');
197
204
  }
198
205
 
199
- const decodedSegments: string[] = [];
206
+ if (screens === undefined) {
207
+ // When no config is specified, use the path segments as route names
208
+ const routes: ParsedRoute[] = [];
200
209
 
201
- for (const segment of remaining.split('/')) {
202
- if (!segment) {
203
- continue;
204
- }
210
+ for (const segment of remaining.split('/')) {
211
+ if (!segment) {
212
+ continue;
213
+ }
205
214
 
206
- try {
207
- decodedSegments.push(decodeURIComponent(segment));
208
- } catch {
209
- return undefined;
215
+ try {
216
+ routes.push({ name: decodeURIComponent(segment) });
217
+ } catch {
218
+ return undefined;
219
+ }
210
220
  }
211
- }
212
-
213
- if (screens === undefined) {
214
- // When no config is specified, use the path segments as route names
215
- const routes = decodedSegments.map((name) => ({ name }));
216
221
 
217
222
  if (routes.length) {
218
223
  return createNestedStateObject(path, routes, initialRoutes);
@@ -224,7 +229,7 @@ export function getStateFromPath<ParamList extends {}>(
224
229
  if (remaining === '/') {
225
230
  // We need to add special handling of empty path so navigation to empty path also works
226
231
  // When handling empty path, we should only look at the root level config
227
- const match = configs.find((config) => config.segments.join('/') === '');
232
+ const match = configs.find((config) => config.pattern === '');
228
233
 
229
234
  if (match) {
230
235
  const config = resolveSharedConfig(match);
@@ -242,7 +247,29 @@ export function getStateFromPath<ParamList extends {}>(
242
247
 
243
248
  // We match the whole path against the regex instead of segments
244
249
  // This makes sure matches such as wildcard will catch any unmatched routes, even if nested
250
+ const firstRawSegment = remaining.split('/')[0];
251
+
252
+ let firstDecodedSegment: string | undefined;
253
+
254
+ try {
255
+ firstDecodedSegment =
256
+ firstRawSegment != null ? decodeURIComponent(firstRawSegment) : undefined;
257
+ } catch {
258
+ firstDecodedSegment = undefined;
259
+ }
260
+
245
261
  for (const config of configs) {
262
+ // Regex matching is expensive, so skip configs that can't match this path.
263
+ if (
264
+ !canMatchFirstSegment(
265
+ config.segments[0],
266
+ firstDecodedSegment,
267
+ firstRawSegment
268
+ )
269
+ ) {
270
+ continue;
271
+ }
272
+
246
273
  const routes = matchAgainstConfig(remaining, config, configsByScreen);
247
274
 
248
275
  if (routes === undefined) {
@@ -281,6 +308,26 @@ export function getStateFromPath<ParamList extends {}>(
281
308
  return undefined;
282
309
  }
283
310
 
311
+ const canMatchFirstSegment = (
312
+ configSegment: string | undefined,
313
+ decodedSegment: string | undefined,
314
+ rawSegment: string | undefined
315
+ ) => {
316
+ if (decodedSegment === undefined) {
317
+ return true;
318
+ }
319
+
320
+ if (configSegment === undefined) {
321
+ return false;
322
+ }
323
+
324
+ if (configSegment === '*' || configSegment.startsWith(':')) {
325
+ return true;
326
+ }
327
+
328
+ return configSegment === decodedSegment || configSegment === rawSegment;
329
+ };
330
+
284
331
  /**
285
332
  * Reference to the last used config resources. This is used to avoid recomputing the config resources when the options are the same.
286
333
  */
@@ -289,11 +336,17 @@ const cachedConfigResources = new WeakMap<Options<{}>, ConfigResources>();
289
336
  function getConfigResources<ParamList extends {}>(
290
337
  options: Options<ParamList> | undefined
291
338
  ) {
292
- if (!options) return prepareConfigResources();
339
+ if (!options) {
340
+ return prepareConfigResources();
341
+ }
293
342
 
294
343
  const cached = cachedConfigResources.get(options);
295
344
 
296
- if (cached) return cached;
345
+ if (cached) {
346
+ return cached;
347
+ }
348
+
349
+ validatePathConfig(options);
297
350
 
298
351
  const resources = prepareConfigResources(options);
299
352
 
@@ -303,21 +356,23 @@ function getConfigResources<ParamList extends {}>(
303
356
  }
304
357
 
305
358
  function prepareConfigResources(options?: Options<{}>) {
306
- if (options) {
307
- validatePathConfig(options);
308
- }
309
-
310
359
  const initialRoutes = getInitialRoutes(options);
311
360
  const configs = getSortedNormalizedConfigs(initialRoutes, options?.screens);
312
361
 
313
- checkForDuplicatedConfigs(configs);
314
-
315
362
  const configsByScreen: Record<string, RouteConfig[]> = {};
316
363
  const configsByPattern: Record<string, RouteConfig[]> = {};
317
364
 
318
365
  for (const c of configs) {
319
366
  (configsByScreen[c.screen] ??= []).push(c);
320
- (configsByPattern[c.segments.join('/')] ??= []).push(c);
367
+
368
+ const existing = configsByPattern[c.pattern];
369
+
370
+ if (existing) {
371
+ checkForDuplicatedConfigs(existing[existing.length - 1], c);
372
+ existing.push(c);
373
+ } else {
374
+ configsByPattern[c.pattern] = [c];
375
+ }
321
376
  }
322
377
 
323
378
  return {
@@ -346,129 +401,133 @@ function getSortedNormalizedConfigs(
346
401
  screens: Record<string, string | PathConfig<{}>> = {}
347
402
  ) {
348
403
  // Create a normalized configs array which will be easier to use
349
- return ([] as RouteConfig[])
350
- .concat(
351
- ...Object.keys(screens).map((key) =>
352
- createNormalizedConfigs(key, screens, initialRoutes, [], [], [])
353
- )
354
- )
355
- .map((config, order) => ({ ...config, order }))
356
- .sort((a, b) => {
357
- // Sort config from most specific to least specific:
358
- // - more segments
359
- // - static segments
360
- // - params with regex
361
- // - regular params
362
- // - wildcard
363
-
364
- // If 2 patterns are same, move the one with less route names up
365
- // This is an error state, so it's only useful for consistent error messages
366
- if (isArrayEqual(a.segments, b.segments)) {
367
- return b.routeNames.join('>').localeCompare(a.routeNames.join('>'));
368
- }
404
+ const configs: RouteConfig[] = [];
369
405
 
370
- // If one of the patterns starts with the other, it's more exhaustive
371
- // So move it up
372
- if (arrayStartsWith(a.segments, b.segments)) {
373
- return -1;
374
- }
406
+ for (const key in screens) {
407
+ configs.push(
408
+ ...createNormalizedConfigs(key, screens, initialRoutes, [], [], [])
409
+ );
410
+ }
375
411
 
376
- if (arrayStartsWith(b.segments, a.segments)) {
377
- return 1;
378
- }
412
+ for (let order = 0; order < configs.length; order++) {
413
+ const config = configs[order];
379
414
 
380
- for (let i = 0; i < Math.max(a.segments.length, b.segments.length); i++) {
381
- const aSegment = a.segments[i];
382
- const bSegment = b.segments[i];
415
+ if (config) {
416
+ config.order = order;
417
+ }
418
+ }
383
419
 
384
- // if b is longer, b gets higher priority
385
- if (aSegment == null) {
386
- return 1;
387
- }
420
+ return configs.sort((a, b) => {
421
+ // Sort config from most specific to least specific:
422
+ // - more segments
423
+ // - static segments
424
+ // - params with regex
425
+ // - regular params
426
+ // - wildcard
427
+
428
+ // If 2 patterns are same, move the one with less route names up
429
+ // This is an error state, so it's only useful for consistent error messages
430
+ if (isArrayEqual(a.segments, b.segments)) {
431
+ return b.routeNames.join('>').localeCompare(a.routeNames.join('>'));
432
+ }
388
433
 
389
- // if a is longer, a gets higher priority
390
- if (bSegment == null) {
391
- return -1;
392
- }
434
+ // If one of the patterns starts with the other, it's more exhaustive
435
+ // So move it up
436
+ if (arrayStartsWith(a.segments, b.segments)) {
437
+ return -1;
438
+ }
393
439
 
394
- const aWildCard = aSegment === '*';
395
- const bWildCard = bSegment === '*';
396
- const aParam = aSegment.startsWith(':');
397
- const bParam = bSegment.startsWith(':');
398
- const aRegex = aParam && aSegment.includes('(');
399
- const bRegex = bParam && bSegment.includes('(');
440
+ if (arrayStartsWith(b.segments, a.segments)) {
441
+ return 1;
442
+ }
400
443
 
401
- // if both are wildcard or regex, we compare next component
402
- if ((aWildCard && bWildCard) || (aRegex && bRegex)) {
403
- continue;
404
- }
444
+ for (let i = 0; i < Math.max(a.segments.length, b.segments.length); i++) {
445
+ const aSegment = a.segments[i];
446
+ const bSegment = b.segments[i];
405
447
 
406
- // if only a is wildcard, b gets higher priority
407
- if (aWildCard && !bWildCard) {
408
- return 1;
409
- }
448
+ // if b is longer, b gets higher priority
449
+ if (aSegment == null) {
450
+ return 1;
451
+ }
410
452
 
411
- // if only b is wildcard, a gets higher priority
412
- if (bWildCard && !aWildCard) {
413
- return -1;
414
- }
453
+ // if a is longer, a gets higher priority
454
+ if (bSegment == null) {
455
+ return -1;
456
+ }
415
457
 
416
- // If only a has a param, b gets higher priority
417
- if (aParam && !bParam) {
418
- return 1;
419
- }
458
+ const aWildCard = aSegment === '*';
459
+ const bWildCard = bSegment === '*';
460
+ const aParam = aSegment.startsWith(':');
461
+ const bParam = bSegment.startsWith(':');
462
+ const aRegex = aParam && aSegment.includes('(');
463
+ const bRegex = bParam && bSegment.includes('(');
420
464
 
421
- // If only b has a param, a gets higher priority
422
- if (bParam && !aParam) {
423
- return -1;
424
- }
465
+ // if both are wildcard or regex, we compare next component
466
+ if ((aWildCard && bWildCard) || (aRegex && bRegex)) {
467
+ continue;
468
+ }
425
469
 
426
- // if only a has regex, a gets higher priority
427
- if (aRegex && !bRegex) {
428
- return -1;
429
- }
470
+ // if only a is wildcard, b gets higher priority
471
+ if (aWildCard && !bWildCard) {
472
+ return 1;
473
+ }
430
474
 
431
- // if only b has regex, b gets higher priority
432
- if (bRegex && !aRegex) {
433
- return 1;
434
- }
475
+ // if only b is wildcard, a gets higher priority
476
+ if (bWildCard && !aWildCard) {
477
+ return -1;
435
478
  }
436
479
 
437
- return a.segments.length - b.segments.length;
438
- });
439
- }
480
+ // If only a has a param, b gets higher priority
481
+ if (aParam && !bParam) {
482
+ return 1;
483
+ }
440
484
 
441
- function checkForDuplicatedConfigs(configs: RouteConfig[]) {
442
- // Check for duplicate patterns in the config
443
- configs.reduce<Record<string, RouteConfig>>((acc, config) => {
444
- const pattern = config.segments.join('/');
445
-
446
- if (acc[pattern]) {
447
- const a = acc[pattern].routeNames;
448
- const b = config.routeNames;
449
-
450
- // It's not a problem if the path string omitted from a inner most screen
451
- // For example, it's ok if a path resolves to `A > B > C` or `A > B`
452
- const intersects =
453
- a.length > b.length
454
- ? b.every((it, i) => a[i] === it)
455
- : a.every((it, i) => b[i] === it);
456
-
457
- if (!intersects && (!acc[pattern].shared || !config.shared)) {
458
- throw new Error(
459
- `Found conflicting screens with the same pattern. The pattern '${
460
- pattern
461
- }' resolves to both '${a.join(' > ')}' and '${b.join(
462
- ' > '
463
- )}'. Patterns must be unique and cannot resolve to more than one screen unless shared: true is specified.`
464
- );
485
+ // If only b has a param, a gets higher priority
486
+ if (bParam && !aParam) {
487
+ return -1;
488
+ }
489
+
490
+ // if only a has regex, a gets higher priority
491
+ if (aRegex && !bRegex) {
492
+ return -1;
493
+ }
494
+
495
+ // if only b has regex, b gets higher priority
496
+ if (bRegex && !aRegex) {
497
+ return 1;
465
498
  }
466
499
  }
467
500
 
468
- return Object.assign(acc, {
469
- [pattern]: config,
470
- });
471
- }, {});
501
+ return a.segments.length - b.segments.length;
502
+ });
503
+ }
504
+
505
+ // Throw if two configs resolve to the same pattern but conflicting screens
506
+ function checkForDuplicatedConfigs(
507
+ existing: RouteConfig | undefined,
508
+ config: RouteConfig
509
+ ) {
510
+ if (!existing) {
511
+ return;
512
+ }
513
+
514
+ const a = existing.routeNames;
515
+ const b = config.routeNames;
516
+
517
+ // It's not a problem if the path string omitted from a inner most screen
518
+ // For example, it's ok if a path resolves to `A > B > C` or `A > B`
519
+ const intersects =
520
+ a.length > b.length ? arrayStartsWith(a, b) : arrayStartsWith(b, a);
521
+
522
+ if (!intersects && (!existing.shared || !config.shared)) {
523
+ throw new Error(
524
+ `Found conflicting screens with the same pattern. The pattern '${
525
+ config.pattern
526
+ }' resolves to both '${a.join(' > ')}' and '${b.join(
527
+ ' > '
528
+ )}'. Patterns must be unique and cannot resolve to more than one screen unless shared: true is specified.`
529
+ );
530
+ }
472
531
  }
473
532
 
474
533
  const matchAgainstConfig = (
@@ -500,20 +559,21 @@ const matchAgainstConfig = (
500
559
  );
501
560
 
502
561
  let params: Record<string, unknown> | undefined;
562
+ let hasParams = false;
503
563
 
504
564
  if (routeConfig && match.groups) {
505
- const paramEntries: [string, unknown][] = [];
506
-
507
- for (const [key, value] of Object.entries(match.groups)) {
508
- const index = Number(key.replace(PARAM_GROUP_PREFIX, ''));
509
- const param = routeConfig.params.find((it) => it.index === index);
565
+ params = {};
510
566
 
511
- if (param?.screen !== routeName || !param.name) {
567
+ for (const param of routeConfig.params) {
568
+ if (param.screen !== routeName) {
512
569
  continue;
513
570
  }
514
571
 
572
+ const value = match.groups[`${PARAM_GROUP_PREFIX}${param.index}`];
573
+
515
574
  if (value == null) {
516
- paramEntries.push([param.name, undefined]);
575
+ params[param.name] = undefined;
576
+ hasParams = true;
517
577
  continue;
518
578
  }
519
579
 
@@ -528,7 +588,8 @@ const matchAgainstConfig = (
528
588
  const parser = routeConfig.parse?.[param.name];
529
589
 
530
590
  if (!parser) {
531
- paramEntries.push([param.name, decoded]);
591
+ params[param.name] = decoded;
592
+ hasParams = true;
532
593
  continue;
533
594
  }
534
595
 
@@ -542,12 +603,14 @@ const matchAgainstConfig = (
542
603
  break;
543
604
  }
544
605
 
545
- paramEntries.push([param.name, result.value]);
606
+ params[param.name] = result.value;
607
+ hasParams = true;
546
608
  continue;
547
609
  }
548
610
 
549
611
  if (typeof parser === 'function') {
550
- paramEntries.push([param.name, parser(decoded)]);
612
+ params[param.name] = parser(decoded);
613
+ hasParams = true;
551
614
  continue;
552
615
  }
553
616
 
@@ -558,13 +621,9 @@ const matchAgainstConfig = (
558
621
  // A failed param validation invalidates the whole nested route chain for this config.
559
622
  break;
560
623
  }
561
-
562
- if (paramEntries.length) {
563
- params = Object.fromEntries(paramEntries);
564
- }
565
624
  }
566
625
 
567
- if (params && Object.keys(params).length) {
626
+ if (params && hasParams) {
568
627
  matchedRoutes.push({ name: routeName, params });
569
628
  } else {
570
629
  matchedRoutes.push({ name: routeName });
@@ -605,7 +664,10 @@ const createNormalizedConfigs = (
605
664
  // if an object is specified as the value (e.g. Foo: { ... }),
606
665
  // it can have `path` property and
607
666
  // it could have `screens` prop which has nested configs
608
- const nestedScreens = 'screens' in config ? config.screens : undefined;
667
+ const nestedScreens =
668
+ 'screens' in config
669
+ ? (config.screens as Record<string, string | PathConfig<{}>>)
670
+ : undefined;
609
671
  const hasNestedScreens = !!nestedScreens;
610
672
  const currentParts =
611
673
  typeof config.path === 'string'
@@ -683,7 +745,7 @@ const createNormalizedConfigs = (
683
745
  );
684
746
  }
685
747
 
686
- if (hasNestedScreens) {
748
+ if (nestedScreens) {
687
749
  // property `initialRouteName` without `screens` has no purpose
688
750
  if (
689
751
  'initialRouteName' in config &&
@@ -695,10 +757,10 @@ const createNormalizedConfigs = (
695
757
  });
696
758
  }
697
759
 
698
- Object.keys(nestedScreens).forEach((nestedConfig) => {
760
+ for (const nestedConfig in nestedScreens) {
699
761
  const result = createNormalizedConfigs(
700
762
  nestedConfig,
701
- nestedScreens as Record<string, string | PathConfig<{}>>,
763
+ nestedScreens,
702
764
  initials,
703
765
  currentParts,
704
766
  [...parentScreens],
@@ -706,7 +768,7 @@ const createNormalizedConfigs = (
706
768
  );
707
769
 
708
770
  configs.push(...result);
709
- });
771
+ }
710
772
  }
711
773
  }
712
774
 
@@ -723,44 +785,56 @@ const createConfigItem = (
723
785
  hasNestedScreens = false,
724
786
  shared = false
725
787
  ): RouteConfig => {
726
- const regex = parts.length
727
- ? new RegExp(
728
- `^(${parts
729
- .map((it, i) => {
730
- if (it.param) {
731
- const reg = it.regex || '[^/]+';
732
-
733
- return `(((?<${PARAM_GROUP_PREFIX}${i}>${reg})\\/)${it.optional ? '?' : ''})`;
734
- }
735
-
736
- return `${it.segment === '*' ? '.*' : escape(it.segment)}\\/`;
737
- })
738
- .join('')})$`
739
- )
740
- : undefined;
741
-
742
- const segments = parts.map((it) => it.segment);
743
- const params = parts
744
- .map((it, i) =>
745
- it.param
746
- ? {
747
- index: i,
748
- screen: it.screen,
749
- name: it.param,
750
- }
751
- : null
752
- )
753
- .filter((it) => it != null);
788
+ let regex: RegExp | undefined;
789
+
790
+ const segments: string[] = [];
791
+ const params: RouteConfig['params'] = [];
792
+ const pathParamNames = new Set<string>();
793
+
794
+ if (parts.length) {
795
+ let regexString = '^(';
796
+ let index = 0;
797
+
798
+ for (const part of parts) {
799
+ segments.push(part.segment);
800
+
801
+ if (part.param) {
802
+ const reg = part.regex || '[^/]+';
803
+
804
+ regexString += `(((?<${PARAM_GROUP_PREFIX}${index}>${reg})\\/)${part.optional ? '?' : ''})`;
805
+ params.push({
806
+ index,
807
+ screen: part.screen,
808
+ name: part.param,
809
+ });
810
+
811
+ if (part.screen === screen) {
812
+ pathParamNames.add(part.param);
813
+ }
814
+ } else {
815
+ regexString += `${part.segment === '*' ? '.*' : escape(part.segment)}\\/`;
816
+ }
817
+
818
+ index++;
819
+ }
820
+
821
+ regexString += ')$';
822
+ regex = new RegExp(regexString);
823
+ }
824
+
825
+ const pattern = segments.join('/');
754
826
 
755
827
  return {
756
828
  screen,
757
829
  order: 0,
758
830
  regex,
831
+ pattern,
759
832
  segments,
760
833
  params,
761
834
  routeNames,
762
835
  parse,
763
836
  explicitParamNames: getExplicitParamNames(parse),
837
+ pathParamNames,
764
838
  hasNestedScreens,
765
839
  shared,
766
840
  };
@@ -979,19 +1053,10 @@ const createNestedStateObject = (
979
1053
 
980
1054
  focusedRoute.path = path.replace(/\/$/, '');
981
1055
 
982
- const pathParamNames = new Set(
983
- routeConfig?.params
984
- .filter(
985
- (param): param is { screen: string; name: string; index: number } =>
986
- param.screen === routeConfig.screen && typeof param.name === 'string'
987
- )
988
- .map((param) => param.name)
989
- );
990
-
991
1056
  const queryParams = parseQueryParams(
992
1057
  path,
993
1058
  routeConfig?.parse,
994
- pathParamNames,
1059
+ routeConfig?.pathParamNames,
995
1060
  routeConfig?.explicitParamNames,
996
1061
  routeConfig?.hasNestedScreens,
997
1062
  focusedRoute.params
@@ -1028,7 +1093,9 @@ const parseQueryParams = (
1028
1093
  }
1029
1094
 
1030
1095
  if (parseConfig) {
1031
- for (const [name, parser] of Object.entries(parseConfig)) {
1096
+ for (const name in parseConfig) {
1097
+ const parser = parseConfig[name];
1098
+
1032
1099
  if (!parser || pathParamNames.has(name)) {
1033
1100
  continue;
1034
1101
  }