@react-navigation/core 7.1.2 → 7.2.0

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 (33) hide show
  1. package/lib/commonjs/arrayStartsWith.js +16 -0
  2. package/lib/commonjs/arrayStartsWith.js.map +1 -0
  3. package/lib/commonjs/getPathFromState.js +50 -40
  4. package/lib/commonjs/getPathFromState.js.map +1 -1
  5. package/lib/commonjs/getPatternParts.js +105 -0
  6. package/lib/commonjs/getPatternParts.js.map +1 -0
  7. package/lib/commonjs/getStateFromPath.js +106 -98
  8. package/lib/commonjs/getStateFromPath.js.map +1 -1
  9. package/lib/module/arrayStartsWith.js +12 -0
  10. package/lib/module/arrayStartsWith.js.map +1 -0
  11. package/lib/module/getPathFromState.js +50 -40
  12. package/lib/module/getPathFromState.js.map +1 -1
  13. package/lib/module/getPatternParts.js +101 -0
  14. package/lib/module/getPatternParts.js.map +1 -0
  15. package/lib/module/getStateFromPath.js +106 -98
  16. package/lib/module/getStateFromPath.js.map +1 -1
  17. package/lib/typescript/commonjs/src/arrayStartsWith.d.ts +5 -0
  18. package/lib/typescript/commonjs/src/arrayStartsWith.d.ts.map +1 -0
  19. package/lib/typescript/commonjs/src/getPathFromState.d.ts.map +1 -1
  20. package/lib/typescript/commonjs/src/getPatternParts.d.ts +11 -0
  21. package/lib/typescript/commonjs/src/getPatternParts.d.ts.map +1 -0
  22. package/lib/typescript/commonjs/src/getStateFromPath.d.ts.map +1 -1
  23. package/lib/typescript/module/src/arrayStartsWith.d.ts +5 -0
  24. package/lib/typescript/module/src/arrayStartsWith.d.ts.map +1 -0
  25. package/lib/typescript/module/src/getPathFromState.d.ts.map +1 -1
  26. package/lib/typescript/module/src/getPatternParts.d.ts +11 -0
  27. package/lib/typescript/module/src/getPatternParts.d.ts.map +1 -0
  28. package/lib/typescript/module/src/getStateFromPath.d.ts.map +1 -1
  29. package/package.json +2 -2
  30. package/src/arrayStartsWith.tsx +10 -0
  31. package/src/getPathFromState.tsx +61 -58
  32. package/src/getPatternParts.tsx +126 -0
  33. package/src/getStateFromPath.tsx +142 -153
@@ -6,7 +6,10 @@ import type {
6
6
  import escape from 'escape-string-regexp';
7
7
  import * as queryString from 'query-string';
8
8
 
9
+ import { arrayStartsWith } from './arrayStartsWith';
9
10
  import { findFocusedRoute } from './findFocusedRoute';
11
+ import { getPatternParts, type PatternPart } from './getPatternParts';
12
+ import { isArrayEqual } from './isArrayEqual';
10
13
  import type { PathConfigMap } from './types';
11
14
  import { validatePathConfig } from './validatePathConfig';
12
15
 
@@ -16,13 +19,13 @@ type Options<ParamList extends {}> = {
16
19
  screens: PathConfigMap<ParamList>;
17
20
  };
18
21
 
19
- type ParseConfig = Record<string, (value: string) => any>;
22
+ type ParseConfig = Record<string, (value: string) => unknown>;
20
23
 
21
24
  type RouteConfig = {
22
25
  screen: string;
23
26
  regex?: RegExp;
24
- path: string;
25
- pattern: string;
27
+ segments: string[];
28
+ params: { screen: string; name?: string; index: number }[];
26
29
  routeNames: string[];
27
30
  parse?: ParseConfig;
28
31
  };
@@ -39,7 +42,7 @@ type ResultState = PartialState<NavigationState> & {
39
42
  type ParsedRoute = {
40
43
  name: string;
41
44
  path?: string;
42
- params?: Record<string, any> | undefined;
45
+ params?: Record<string, unknown> | undefined;
43
46
  };
44
47
 
45
48
  type ConfigResources = {
@@ -121,14 +124,7 @@ export function getStateFromPath<ParamList extends {}>(
121
124
  if (remaining === '/') {
122
125
  // We need to add special handling of empty path so navigation to empty path also works
123
126
  // When handling empty path, we should only look at the root level config
124
- const match = configs.find(
125
- (config) =>
126
- config.path === '' &&
127
- config.routeNames.every(
128
- // Make sure that none of the parent configs have a non-empty path defined
129
- (name) => !configs.find((c) => c.screen === name)?.path
130
- )
131
- );
127
+ const match = configs.find((config) => config.segments.join('/') === '');
132
128
 
133
129
  if (match) {
134
130
  return createNestedStateObject(
@@ -231,8 +227,9 @@ function getNormalizedConfigs(
231
227
  createNormalizedConfigs(
232
228
  key,
233
229
  screens as PathConfigMap<object>,
234
- [],
235
230
  initialRoutes,
231
+ [],
232
+ [],
236
233
  []
237
234
  )
238
235
  )
@@ -244,56 +241,77 @@ function getNormalizedConfigs(
244
241
 
245
242
  // If 2 patterns are same, move the one with less route names up
246
243
  // This is an error state, so it's only useful for consistent error messages
247
- if (a.pattern === b.pattern) {
244
+ if (isArrayEqual(a.segments, b.segments)) {
248
245
  return b.routeNames.join('>').localeCompare(a.routeNames.join('>'));
249
246
  }
250
247
 
251
248
  // If one of the patterns starts with the other, it's more exhaustive
252
249
  // So move it up
253
- if (a.pattern.startsWith(b.pattern)) {
250
+ if (arrayStartsWith(a.segments, b.segments)) {
254
251
  return -1;
255
252
  }
256
253
 
257
- if (b.pattern.startsWith(a.pattern)) {
254
+ if (arrayStartsWith(b.segments, a.segments)) {
258
255
  return 1;
259
256
  }
260
257
 
261
- const aParts = a.pattern.split('/');
262
- const bParts = b.pattern.split('/');
263
-
264
- for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
265
- // if b is longer, b get higher priority
266
- if (aParts[i] == null) {
258
+ for (let i = 0; i < Math.max(a.segments.length, b.segments.length); i++) {
259
+ // if b is longer, b gets higher priority
260
+ if (a.segments[i] == null) {
267
261
  return 1;
268
262
  }
269
- // if a is longer, a get higher priority
270
- if (bParts[i] == null) {
263
+
264
+ // if a is longer, a gets higher priority
265
+ if (b.segments[i] == null) {
271
266
  return -1;
272
267
  }
273
- const aWildCard = aParts[i] === '*' || aParts[i].startsWith(':');
274
- const bWildCard = bParts[i] === '*' || bParts[i].startsWith(':');
275
- // if both are wildcard we compare next component
276
- if (aWildCard && bWildCard) {
268
+
269
+ const aWildCard =
270
+ a.segments[i] === '*' || a.segments[i].startsWith(':');
271
+ const bWildCard =
272
+ b.segments[i] === '*' || b.segments[i].startsWith(':');
273
+ const aRegex =
274
+ a.segments[i].startsWith(':') && a.segments[i].includes('(');
275
+ const bRegex =
276
+ b.segments[i].startsWith(':') && b.segments[i].includes('(');
277
+
278
+ // if both are wildcard & regex we compare next component
279
+ if (aWildCard && bWildCard && aRegex && bRegex) {
277
280
  continue;
278
281
  }
279
- // if only a is wild card, b get higher priority
280
- if (aWildCard) {
282
+
283
+ // if only a has regex, a gets higher priority
284
+ if (aRegex && !bRegex) {
285
+ return -1;
286
+ }
287
+
288
+ // if only b has regex, b gets higher priority
289
+ if (bRegex && !aRegex) {
281
290
  return 1;
282
291
  }
283
- // if only b is wild card, a get higher priority
284
- if (bWildCard) {
292
+
293
+ // if only a is wildcard, b gets higher priority
294
+ if (aWildCard && !bWildCard) {
295
+ return 1;
296
+ }
297
+
298
+ // if only b is wildcard, a gets higher priority
299
+ if (bWildCard && !aWildCard) {
285
300
  return -1;
286
301
  }
287
302
  }
288
- return bParts.length - aParts.length;
303
+
304
+ return a.segments.length - b.segments.length;
289
305
  });
290
306
  }
291
307
 
292
308
  function checkForDuplicatedConfigs(configs: RouteConfig[]) {
293
309
  // Check for duplicate patterns in the config
294
310
  configs.reduce<Record<string, RouteConfig>>((acc, config) => {
295
- if (acc[config.pattern]) {
296
- const a = acc[config.pattern].routeNames;
311
+ const pattern = config.segments.join('/');
312
+
313
+ if (acc[pattern]) {
314
+ const a = acc[pattern].routeNames;
297
315
  const b = config.routeNames;
298
316
 
299
317
  // It's not a problem if the path string omitted from a inner most screen
@@ -306,7 +324,7 @@ function checkForDuplicatedConfigs(configs: RouteConfig[]) {
306
324
  if (!intersects) {
307
325
  throw new Error(
308
326
  `Found conflicting screens with the same pattern. The pattern '${
309
- config.pattern
327
+ pattern
310
328
  }' resolves to both '${a.join(' > ')}' and '${b.join(
311
329
  ' > '
312
330
  )}'. Patterns must be unique and cannot resolve to more than one screen.`
@@ -315,7 +333,7 @@ function checkForDuplicatedConfigs(configs: RouteConfig[]) {
315
333
  }
316
334
 
317
335
  return Object.assign(acc, {
318
- [config.pattern]: config,
336
+ [pattern]: config,
319
337
  });
320
338
  }, {});
321
339
  }
@@ -328,12 +346,6 @@ function getConfigsWithRegexes(configs: RouteConfig[]) {
328
346
  }));
329
347
  }
330
348
 
331
- const joinPaths = (...paths: string[]): string =>
332
- ([] as string[])
333
- .concat(...paths.map((p) => p.split('/')))
334
- .filter(Boolean)
335
- .join('/');
336
-
337
349
  const matchAgainstConfigs = (remaining: string, configs: RouteConfig[]) => {
338
350
  let routes: ParsedRoute[] | undefined;
339
351
  let remainingPath = remaining;
@@ -348,86 +360,55 @@ const matchAgainstConfigs = (remaining: string, configs: RouteConfig[]) => {
348
360
 
349
361
  // If our regex matches, we need to extract params from the path
350
362
  if (match) {
351
- const matchResult = config.pattern?.split('/').reduce<{
352
- pos: number; // Position of the current path param segment in the path (e.g in pattern `a/:b/:c`, `:a` is 0 and `:b` is 1)
353
- matchedParams: Record<string, Record<string, string>>; // The extracted params
354
- }>(
355
- (acc, p, index) => {
356
- if (!p.startsWith(':')) {
357
- return acc;
358
- }
359
-
360
- // Path parameter so increment position for the segment
361
- acc.pos += 1;
362
-
363
- const decodedParamSegment = decodeURIComponent(
364
- // The param segments appear every second item starting from 2 in the regex match result
365
- match![(acc.pos + 1) * 2]
366
- // Remove trailing slash
367
- .replace(/\/$/, '')
368
- );
369
-
370
- Object.assign(acc.matchedParams, {
371
- [p]: Object.assign(acc.matchedParams[p] || {}, {
372
- [index]: decodedParamSegment,
373
- }),
374
- });
375
-
376
- return acc;
377
- },
378
- { pos: -1, matchedParams: {} }
379
- );
380
-
381
- const matchedParams = matchResult.matchedParams || {};
382
-
383
- routes = config.routeNames.map((name) => {
363
+ routes = config.routeNames.map((routeName) => {
384
364
  const routeConfig = configs.find((c) => {
385
365
  // Check matching name AND pattern in case same screen is used at different levels in config
386
- return c.screen === name && config.pattern.startsWith(c.pattern);
366
+ return (
367
+ c.screen === routeName &&
368
+ arrayStartsWith(config.segments, c.segments)
369
+ );
387
370
  });
388
371
 
389
- // Normalize pattern to remove any leading, trailing slashes, duplicate slashes etc.
390
- const normalizedPath = routeConfig?.path
391
- .split('/')
392
- .filter(Boolean)
393
- .join('/');
394
-
395
- // Get the number of segments in the initial pattern
396
- const numInitialSegments = routeConfig?.pattern
397
- // Extract the prefix from the pattern by removing the ending path pattern (e.g pattern=`a/b/c/d` and normalizedPath=`c/d` becomes `a/b`)
398
- .replace(new RegExp(`${escape(normalizedPath!)}$`), '')
399
- ?.split('/').length;
400
-
401
- const params = normalizedPath
402
- ?.split('/')
403
- .reduce<Record<string, unknown>>((acc, p, index) => {
404
- if (!p.startsWith(':')) {
405
- return acc;
406
- }
407
-
408
- // Get the real index of the path parameter in the matched path
409
- // by offsetting by the number of segments in the initial pattern
410
- const offset = numInitialSegments ? numInitialSegments - 1 : 0;
411
- const value = matchedParams[p]?.[index + offset];
412
-
413
- if (value) {
414
- const key = p.replace(/^:/, '').replace(/\?$/, '');
415
- acc[key] = routeConfig?.parse?.[key]
416
- ? routeConfig.parse[key](value)
417
- : value;
418
- }
419
-
420
- return acc;
421
- }, {});
372
+ const params =
373
+ routeConfig && match.groups
374
+ ? Object.fromEntries(
375
+ Object.entries(match.groups)
376
+ .map(([key, value]) => {
377
+ const index = Number(key.replace('param_', ''));
378
+ const param = routeConfig.params.find(
379
+ (it) => it.index === index
380
+ );
381
+
382
+ if (param?.screen === routeName && param?.name) {
383
+ return [param.name, value];
384
+ }
385
+
386
+ return null;
387
+ })
388
+ .filter((it) => it != null)
389
+ .map(([key, value]) => {
390
+ if (value == null) {
391
+ return [key, undefined];
392
+ }
393
+
394
+ const decoded = decodeURIComponent(value);
395
+ const parsed = routeConfig.parse?.[key]
396
+ ? routeConfig.parse[key](decoded)
397
+ : decoded;
398
+
399
+ return [key, parsed];
400
+ })
401
+ )
402
+ : undefined;
422
403
 
423
404
  if (params && Object.keys(params).length) {
424
- return { name, params };
405
+ return { name: routeName, params };
425
406
  }
426
407
 
427
- return { name };
408
+ return { name: routeName };
428
409
  });
429
410
 
430
- remainingPath = remainingPath.replace(match[1], '');
411
+ remainingPath = remainingPath.replace(match[0], '');
431
412
 
432
413
  break;
433
414
  }
@@ -439,10 +420,10 @@ const matchAgainstConfigs = (remaining: string, configs: RouteConfig[]) => {
439
420
  const createNormalizedConfigs = (
440
421
  screen: string,
441
422
  routeConfig: PathConfigMap<object>,
442
- routeNames: string[] = [],
443
423
  initials: InitialRouteConfig[],
424
+ paths: { screen: string; path: string }[],
444
425
  parentScreens: string[],
445
- parentPattern?: string
426
+ routeNames: string[]
446
427
  ): RouteConfig[] => {
447
428
  const configs: RouteConfig[] = [];
448
429
 
@@ -454,13 +435,9 @@ const createNormalizedConfigs = (
454
435
  const config = routeConfig[screen];
455
436
 
456
437
  if (typeof config === 'string') {
457
- // If a string is specified as the value of the key(e.g. Foo: '/path'), use it as the pattern
458
- const pattern = parentPattern ? joinPaths(parentPattern, config) : config;
459
-
460
- configs.push(createConfigItem(screen, routeNames, pattern, config));
438
+ paths.push({ screen, path: config });
439
+ configs.push(createConfigItem(screen, [...routeNames], [...paths]));
461
440
  } else if (typeof config === 'object') {
462
- let pattern: string | undefined;
463
-
464
441
  // if an object is specified as the value (e.g. Foo: { ... }),
465
442
  // it can have `path` property and
466
443
  // it could have `screens` prop which has nested configs
@@ -471,19 +448,15 @@ const createNormalizedConfigs = (
471
448
  );
472
449
  }
473
450
 
474
- pattern =
475
- config.exact !== true
476
- ? joinPaths(parentPattern || '', config.path || '')
477
- : config.path || '';
451
+ if (config.exact) {
452
+ // If it's an exact path, we don't need to keep track of the parent screens
453
+ // So we can clear it
454
+ paths.length = 0;
455
+ }
478
456
 
457
+ paths.push({ screen, path: config.path });
479
458
  configs.push(
480
- createConfigItem(
481
- screen,
482
- routeNames,
483
- pattern!,
484
- config.path,
485
- config.parse
486
- )
459
+ createConfigItem(screen, [...routeNames], [...paths], config.parse)
487
460
  );
488
461
  }
489
462
 
@@ -500,10 +473,10 @@ const createNormalizedConfigs = (
500
473
  const result = createNormalizedConfigs(
501
474
  nestedConfig,
502
475
  config.screens as PathConfigMap<object>,
503
- routeNames,
504
476
  initials,
477
+ [...paths],
505
478
  [...parentScreens],
506
- pattern ?? parentPattern
479
+ routeNames
507
480
  );
508
481
 
509
482
  configs.push(...result);
@@ -519,35 +492,51 @@ const createNormalizedConfigs = (
519
492
  const createConfigItem = (
520
493
  screen: string,
521
494
  routeNames: string[],
522
- pattern: string,
523
- path: string,
495
+ paths: { screen: string; path: string }[],
524
496
  parse?: ParseConfig
525
497
  ): RouteConfig => {
526
- // Normalize pattern to remove any leading, trailing slashes, duplicate slashes etc.
527
- pattern = pattern.split('/').filter(Boolean).join('/');
498
+ const parts: (PatternPart & { screen: string })[] = [];
499
+
500
+ // Parse the path string into parts for easier matching
501
+ for (const { screen, path } of paths) {
502
+ parts.push(...getPatternParts(path).map((part) => ({ ...part, screen })));
503
+ }
528
504
 
529
- const regex = pattern
505
+ const regex = parts.length
530
506
  ? new RegExp(
531
- `^(${pattern
532
- .split('/')
533
- .map((it) => {
534
- if (it.startsWith(':')) {
535
- return `(([^/]+\\/)${it.endsWith('?') ? '?' : ''})`;
507
+ `^(${parts
508
+ .map((it, i) => {
509
+ if (it.param) {
510
+ const reg = it.regex || '[^/]+';
511
+
512
+ return `(((?<param_${i}>${reg})\\/)${it.optional ? '?' : ''})`;
536
513
  }
537
514
 
538
- return `${it === '*' ? '.*' : escape(it)}\\/`;
515
+ return `${it.segment === '*' ? '.*' : escape(it.segment)}\\/`;
539
516
  })
540
517
  .join('')})`
541
518
  )
542
519
  : undefined;
543
520
 
521
+ const segments = parts.map((it) => it.segment);
522
+ const params = parts
523
+ .map((it, i) =>
524
+ it.param
525
+ ? {
526
+ index: i,
527
+ screen: it.screen,
528
+ name: it.param,
529
+ }
530
+ : null
531
+ )
532
+ .filter((it) => it != null);
533
+
544
534
  return {
545
535
  screen,
546
536
  regex,
547
- pattern,
548
- path,
549
- // The routeNames array is mutated, so copy it to keep the current state
550
- routeNames: [...routeNames],
537
+ segments,
538
+ params,
539
+ routeNames,
551
540
  parse,
552
541
  };
553
542
  };
@@ -666,7 +655,7 @@ const createNestedStateObject = (
666
655
  }
667
656
 
668
657
  route = findFocusedRoute(state) as ParsedRoute;
669
- route.path = path;
658
+ route.path = path.replace(/\/$/, '');
670
659
 
671
660
  const params = parseQueryParams(
672
661
  path,
@@ -682,10 +671,10 @@ const createNestedStateObject = (
682
671
 
683
672
  const parseQueryParams = (
684
673
  path: string,
685
- parseConfig?: Record<string, (value: string) => any>
674
+ parseConfig?: Record<string, (value: string) => unknown>
686
675
  ) => {
687
676
  const query = path.split('?')[1];
688
- const params = queryString.parse(query);
677
+ const params: Record<string, unknown> = queryString.parse(query);
689
678
 
690
679
  if (parseConfig) {
691
680
  Object.keys(params).forEach((name) => {
@@ -693,7 +682,7 @@ const parseQueryParams = (
693
682
  Object.hasOwnProperty.call(parseConfig, name) &&
694
683
  typeof params[name] === 'string'
695
684
  ) {
696
- params[name] = parseConfig[name](params[name] as string);
685
+ params[name] = parseConfig[name](params[name]);
697
686
  }
698
687
  });
699
688
  }