@react-navigation/core 7.1.2 → 7.2.1

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 +124 -101
  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 +124 -101
  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 +156 -156
@@ -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,69 +227,102 @@ 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
  )
239
236
  )
240
237
  .sort((a, b) => {
241
- // Sort config so that:
242
- // - the most exhaustive ones are always at the beginning
243
- // - patterns with wildcard are always at the end
238
+ // Sort config from most specific to least specific:
239
+ // - more segments
240
+ // - static segments
241
+ // - params with regex
242
+ // - regular params
243
+ // - wildcard
244
244
 
245
245
  // If 2 patterns are same, move the one with less route names up
246
246
  // This is an error state, so it's only useful for consistent error messages
247
- if (a.pattern === b.pattern) {
247
+ if (isArrayEqual(a.segments, b.segments)) {
248
248
  return b.routeNames.join('>').localeCompare(a.routeNames.join('>'));
249
249
  }
250
250
 
251
251
  // If one of the patterns starts with the other, it's more exhaustive
252
252
  // So move it up
253
- if (a.pattern.startsWith(b.pattern)) {
253
+ if (arrayStartsWith(a.segments, b.segments)) {
254
254
  return -1;
255
255
  }
256
256
 
257
- if (b.pattern.startsWith(a.pattern)) {
257
+ if (arrayStartsWith(b.segments, a.segments)) {
258
258
  return 1;
259
259
  }
260
260
 
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) {
261
+ for (let i = 0; i < Math.max(a.segments.length, b.segments.length); i++) {
262
+ // if b is longer, b gets higher priority
263
+ if (a.segments[i] == null) {
267
264
  return 1;
268
265
  }
269
- // if a is longer, a get higher priority
270
- if (bParts[i] == null) {
266
+
267
+ // if a is longer, a gets higher priority
268
+ if (b.segments[i] == null) {
271
269
  return -1;
272
270
  }
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) {
271
+
272
+ const aWildCard = a.segments[i] === '*';
273
+ const bWildCard = b.segments[i] === '*';
274
+ const aParam = a.segments[i].startsWith(':');
275
+ const bParam = b.segments[i].startsWith(':');
276
+ const aRegex = aParam && a.segments[i].includes('(');
277
+ const bRegex = bParam && b.segments[i].includes('(');
278
+
279
+ // if both are wildcard or regex, we compare next component
280
+ if ((aWildCard && bWildCard) || (aRegex && bRegex)) {
277
281
  continue;
278
282
  }
279
- // if only a is wild card, b get higher priority
280
- if (aWildCard) {
283
+
284
+ // If only a has a param, b gets higher priority
285
+ if (aParam && !bParam) {
281
286
  return 1;
282
287
  }
283
- // if only b is wild card, a get higher priority
284
- if (bWildCard) {
288
+
289
+ // If only b has a param, a gets higher priority
290
+ if (bParam && !aParam) {
291
+ return -1;
292
+ }
293
+
294
+ // if only a has regex, a gets higher priority
295
+ if (aRegex && !bRegex) {
296
+ return -1;
297
+ }
298
+
299
+ // if only b has regex, b gets higher priority
300
+ if (bRegex && !aRegex) {
301
+ return 1;
302
+ }
303
+
304
+ // if only a is wildcard, b gets higher priority
305
+ if (aWildCard && !bWildCard) {
306
+ return 1;
307
+ }
308
+
309
+ // if only b is wildcard, a gets higher priority
310
+ if (bWildCard && !aWildCard) {
285
311
  return -1;
286
312
  }
287
313
  }
288
- return bParts.length - aParts.length;
314
+
315
+ return a.segments.length - b.segments.length;
289
316
  });
290
317
  }
291
318
 
292
319
  function checkForDuplicatedConfigs(configs: RouteConfig[]) {
293
320
  // Check for duplicate patterns in the config
294
321
  configs.reduce<Record<string, RouteConfig>>((acc, config) => {
295
- if (acc[config.pattern]) {
296
- const a = acc[config.pattern].routeNames;
322
+ const pattern = config.segments.join('/');
323
+
324
+ if (acc[pattern]) {
325
+ const a = acc[pattern].routeNames;
297
326
  const b = config.routeNames;
298
327
 
299
328
  // It's not a problem if the path string omitted from a inner most screen
@@ -306,7 +335,7 @@ function checkForDuplicatedConfigs(configs: RouteConfig[]) {
306
335
  if (!intersects) {
307
336
  throw new Error(
308
337
  `Found conflicting screens with the same pattern. The pattern '${
309
- config.pattern
338
+ pattern
310
339
  }' resolves to both '${a.join(' > ')}' and '${b.join(
311
340
  ' > '
312
341
  )}'. Patterns must be unique and cannot resolve to more than one screen.`
@@ -315,7 +344,7 @@ function checkForDuplicatedConfigs(configs: RouteConfig[]) {
315
344
  }
316
345
 
317
346
  return Object.assign(acc, {
318
- [config.pattern]: config,
347
+ [pattern]: config,
319
348
  });
320
349
  }, {});
321
350
  }
@@ -328,12 +357,6 @@ function getConfigsWithRegexes(configs: RouteConfig[]) {
328
357
  }));
329
358
  }
330
359
 
331
- const joinPaths = (...paths: string[]): string =>
332
- ([] as string[])
333
- .concat(...paths.map((p) => p.split('/')))
334
- .filter(Boolean)
335
- .join('/');
336
-
337
360
  const matchAgainstConfigs = (remaining: string, configs: RouteConfig[]) => {
338
361
  let routes: ParsedRoute[] | undefined;
339
362
  let remainingPath = remaining;
@@ -348,86 +371,55 @@ const matchAgainstConfigs = (remaining: string, configs: RouteConfig[]) => {
348
371
 
349
372
  // If our regex matches, we need to extract params from the path
350
373
  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) => {
374
+ routes = config.routeNames.map((routeName) => {
384
375
  const routeConfig = configs.find((c) => {
385
376
  // 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);
377
+ return (
378
+ c.screen === routeName &&
379
+ arrayStartsWith(config.segments, c.segments)
380
+ );
387
381
  });
388
382
 
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
- }, {});
383
+ const params =
384
+ routeConfig && match.groups
385
+ ? Object.fromEntries(
386
+ Object.entries(match.groups)
387
+ .map(([key, value]) => {
388
+ const index = Number(key.replace('param_', ''));
389
+ const param = routeConfig.params.find(
390
+ (it) => it.index === index
391
+ );
392
+
393
+ if (param?.screen === routeName && param?.name) {
394
+ return [param.name, value];
395
+ }
396
+
397
+ return null;
398
+ })
399
+ .filter((it) => it != null)
400
+ .map(([key, value]) => {
401
+ if (value == null) {
402
+ return [key, undefined];
403
+ }
404
+
405
+ const decoded = decodeURIComponent(value);
406
+ const parsed = routeConfig.parse?.[key]
407
+ ? routeConfig.parse[key](decoded)
408
+ : decoded;
409
+
410
+ return [key, parsed];
411
+ })
412
+ )
413
+ : undefined;
422
414
 
423
415
  if (params && Object.keys(params).length) {
424
- return { name, params };
416
+ return { name: routeName, params };
425
417
  }
426
418
 
427
- return { name };
419
+ return { name: routeName };
428
420
  });
429
421
 
430
- remainingPath = remainingPath.replace(match[1], '');
422
+ remainingPath = remainingPath.replace(match[0], '');
431
423
 
432
424
  break;
433
425
  }
@@ -439,10 +431,10 @@ const matchAgainstConfigs = (remaining: string, configs: RouteConfig[]) => {
439
431
  const createNormalizedConfigs = (
440
432
  screen: string,
441
433
  routeConfig: PathConfigMap<object>,
442
- routeNames: string[] = [],
443
434
  initials: InitialRouteConfig[],
435
+ paths: { screen: string; path: string }[],
444
436
  parentScreens: string[],
445
- parentPattern?: string
437
+ routeNames: string[]
446
438
  ): RouteConfig[] => {
447
439
  const configs: RouteConfig[] = [];
448
440
 
@@ -454,13 +446,9 @@ const createNormalizedConfigs = (
454
446
  const config = routeConfig[screen];
455
447
 
456
448
  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));
449
+ paths.push({ screen, path: config });
450
+ configs.push(createConfigItem(screen, [...routeNames], [...paths]));
461
451
  } else if (typeof config === 'object') {
462
- let pattern: string | undefined;
463
-
464
452
  // if an object is specified as the value (e.g. Foo: { ... }),
465
453
  // it can have `path` property and
466
454
  // it could have `screens` prop which has nested configs
@@ -471,19 +459,15 @@ const createNormalizedConfigs = (
471
459
  );
472
460
  }
473
461
 
474
- pattern =
475
- config.exact !== true
476
- ? joinPaths(parentPattern || '', config.path || '')
477
- : config.path || '';
462
+ if (config.exact) {
463
+ // If it's an exact path, we don't need to keep track of the parent screens
464
+ // So we can clear it
465
+ paths.length = 0;
466
+ }
478
467
 
468
+ paths.push({ screen, path: config.path });
479
469
  configs.push(
480
- createConfigItem(
481
- screen,
482
- routeNames,
483
- pattern!,
484
- config.path,
485
- config.parse
486
- )
470
+ createConfigItem(screen, [...routeNames], [...paths], config.parse)
487
471
  );
488
472
  }
489
473
 
@@ -500,10 +484,10 @@ const createNormalizedConfigs = (
500
484
  const result = createNormalizedConfigs(
501
485
  nestedConfig,
502
486
  config.screens as PathConfigMap<object>,
503
- routeNames,
504
487
  initials,
488
+ [...paths],
505
489
  [...parentScreens],
506
- pattern ?? parentPattern
490
+ routeNames
507
491
  );
508
492
 
509
493
  configs.push(...result);
@@ -519,35 +503,51 @@ const createNormalizedConfigs = (
519
503
  const createConfigItem = (
520
504
  screen: string,
521
505
  routeNames: string[],
522
- pattern: string,
523
- path: string,
506
+ paths: { screen: string; path: string }[],
524
507
  parse?: ParseConfig
525
508
  ): RouteConfig => {
526
- // Normalize pattern to remove any leading, trailing slashes, duplicate slashes etc.
527
- pattern = pattern.split('/').filter(Boolean).join('/');
509
+ const parts: (PatternPart & { screen: string })[] = [];
528
510
 
529
- const regex = pattern
511
+ // Parse the path string into parts for easier matching
512
+ for (const { screen, path } of paths) {
513
+ parts.push(...getPatternParts(path).map((part) => ({ ...part, screen })));
514
+ }
515
+
516
+ const regex = parts.length
530
517
  ? new RegExp(
531
- `^(${pattern
532
- .split('/')
533
- .map((it) => {
534
- if (it.startsWith(':')) {
535
- return `(([^/]+\\/)${it.endsWith('?') ? '?' : ''})`;
518
+ `^(${parts
519
+ .map((it, i) => {
520
+ if (it.param) {
521
+ const reg = it.regex || '[^/]+';
522
+
523
+ return `(((?<param_${i}>${reg})\\/)${it.optional ? '?' : ''})`;
536
524
  }
537
525
 
538
- return `${it === '*' ? '.*' : escape(it)}\\/`;
526
+ return `${it.segment === '*' ? '.*' : escape(it.segment)}\\/`;
539
527
  })
540
528
  .join('')})`
541
529
  )
542
530
  : undefined;
543
531
 
532
+ const segments = parts.map((it) => it.segment);
533
+ const params = parts
534
+ .map((it, i) =>
535
+ it.param
536
+ ? {
537
+ index: i,
538
+ screen: it.screen,
539
+ name: it.param,
540
+ }
541
+ : null
542
+ )
543
+ .filter((it) => it != null);
544
+
544
545
  return {
545
546
  screen,
546
547
  regex,
547
- pattern,
548
- path,
549
- // The routeNames array is mutated, so copy it to keep the current state
550
- routeNames: [...routeNames],
548
+ segments,
549
+ params,
550
+ routeNames,
551
551
  parse,
552
552
  };
553
553
  };
@@ -666,7 +666,7 @@ const createNestedStateObject = (
666
666
  }
667
667
 
668
668
  route = findFocusedRoute(state) as ParsedRoute;
669
- route.path = path;
669
+ route.path = path.replace(/\/$/, '');
670
670
 
671
671
  const params = parseQueryParams(
672
672
  path,
@@ -682,10 +682,10 @@ const createNestedStateObject = (
682
682
 
683
683
  const parseQueryParams = (
684
684
  path: string,
685
- parseConfig?: Record<string, (value: string) => any>
685
+ parseConfig?: Record<string, (value: string) => unknown>
686
686
  ) => {
687
687
  const query = path.split('?')[1];
688
- const params = queryString.parse(query);
688
+ const params: Record<string, unknown> = queryString.parse(query);
689
689
 
690
690
  if (parseConfig) {
691
691
  Object.keys(params).forEach((name) => {
@@ -693,7 +693,7 @@ const parseQueryParams = (
693
693
  Object.hasOwnProperty.call(parseConfig, name) &&
694
694
  typeof params[name] === 'string'
695
695
  ) {
696
- params[name] = parseConfig[name](params[name] as string);
696
+ params[name] = parseConfig[name](params[name]);
697
697
  }
698
698
  });
699
699
  }