@react-navigation/core 8.0.0-alpha.3 → 8.0.0-alpha.5

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