@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.
- package/lib/module/BaseNavigationContainer.js.map +1 -1
- package/lib/module/NavigationBuilderContext.js.map +1 -1
- package/lib/module/NavigationIndependentTree.js +8 -4
- package/lib/module/NavigationIndependentTree.js.map +1 -1
- package/lib/module/NavigationProvider.js +14 -3
- package/lib/module/NavigationProvider.js.map +1 -1
- package/lib/module/NavigationStateContext.js.map +1 -1
- package/lib/module/SceneView.js +6 -39
- package/lib/module/SceneView.js.map +1 -1
- package/lib/module/StaticNavigation.js +13 -1
- package/lib/module/StaticNavigation.js.map +1 -1
- package/lib/module/getPathFromState.js +24 -2
- package/lib/module/getPathFromState.js.map +1 -1
- package/lib/module/getStateFromPath.js +157 -72
- 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/index.js.map +1 -1
- package/lib/module/types.js.map +1 -1
- package/lib/module/useIsFocused.js +7 -12
- package/lib/module/useIsFocused.js.map +1 -1
- package/lib/module/useNavigationBuilder.js +71 -28
- package/lib/module/useNavigationBuilder.js.map +1 -1
- package/lib/module/useOnAction.js.map +1 -1
- package/lib/module/useOnRouteFocus.js.map +1 -1
- package/lib/typescript/src/NavigationBuilderContext.d.ts +5 -5
- package/lib/typescript/src/NavigationBuilderContext.d.ts.map +1 -1
- package/lib/typescript/src/NavigationFocusedRouteStateContext.d.ts +4 -4
- package/lib/typescript/src/NavigationFocusedRouteStateContext.d.ts.map +1 -1
- package/lib/typescript/src/NavigationIndependentTree.d.ts.map +1 -1
- package/lib/typescript/src/NavigationProvider.d.ts +4 -4
- package/lib/typescript/src/NavigationProvider.d.ts.map +1 -1
- package/lib/typescript/src/NavigationStateContext.d.ts +3 -3
- package/lib/typescript/src/NavigationStateContext.d.ts.map +1 -1
- package/lib/typescript/src/SceneView.d.ts.map +1 -1
- package/lib/typescript/src/StaticNavigation.d.ts +13 -11
- package/lib/typescript/src/StaticNavigation.d.ts.map +1 -1
- package/lib/typescript/src/findFocusedRoute.d.ts +3 -3
- package/lib/typescript/src/findFocusedRoute.d.ts.map +1 -1
- package/lib/typescript/src/getActionFromState.d.ts +3 -3
- package/lib/typescript/src/getActionFromState.d.ts.map +1 -1
- package/lib/typescript/src/getPathFromState.d.ts +2 -2
- package/lib/typescript/src/getPathFromState.d.ts.map +1 -1
- package/lib/typescript/src/getStateFromPath.d.ts +3 -3
- 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/index.d.ts +1 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/types.d.ts +64 -64
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/lib/typescript/src/useDescriptors.d.ts +2 -2
- package/lib/typescript/src/useIsFocused.d.ts +3 -0
- package/lib/typescript/src/useIsFocused.d.ts.map +1 -1
- package/lib/typescript/src/useNavigationBuilder.d.ts +17 -17
- package/lib/typescript/src/useNavigationBuilder.d.ts.map +1 -1
- package/lib/typescript/src/useNavigationHelpers.d.ts +15 -15
- package/lib/typescript/src/useOnAction.d.ts +6 -6
- package/lib/typescript/src/useOnAction.d.ts.map +1 -1
- package/lib/typescript/src/useOnRouteFocus.d.ts +6 -6
- package/lib/typescript/src/useOnRouteFocus.d.ts.map +1 -1
- package/lib/typescript/src/useRouteCache.d.ts +2 -2
- package/lib/typescript/src/utilities.d.ts +35 -3
- package/lib/typescript/src/utilities.d.ts.map +1 -1
- package/package.json +10 -8
- package/src/BaseNavigationContainer.tsx +1 -1
- package/src/NavigationBuilderContext.tsx +7 -8
- package/src/NavigationFocusedRouteStateContext.tsx +4 -4
- package/src/NavigationIndependentTree.tsx +8 -5
- package/src/NavigationProvider.tsx +17 -3
- package/src/NavigationStateContext.tsx +5 -6
- package/src/SceneView.tsx +6 -36
- package/src/StaticNavigation.tsx +48 -17
- package/src/findFocusedRoute.tsx +3 -3
- package/src/getActionFromState.tsx +7 -7
- package/src/getPathFromState.tsx +52 -8
- package/src/getStateFromPath.tsx +254 -96
- package/src/getStateFromRouteParams.tsx +60 -0
- package/src/index.tsx +1 -0
- package/src/types.tsx +164 -120
- package/src/useIsFocused.tsx +14 -21
- package/src/useNavigationBuilder.tsx +116 -41
- package/src/useOnAction.tsx +7 -7
- package/src/useOnRouteFocus.tsx +9 -11
- package/src/utilities.tsx +72 -4
|
@@ -42,6 +42,7 @@ import { type ScreenConfigWithParent, useDescriptors } from './useDescriptors';
|
|
|
42
42
|
import { useEventEmitter } from './useEventEmitter';
|
|
43
43
|
import { useFocusedListenersChildrenAdapter } from './useFocusedListenersChildrenAdapter';
|
|
44
44
|
import { useFocusEvents } from './useFocusEvents';
|
|
45
|
+
import { FocusedRouteKeyContext } from './useIsFocused';
|
|
45
46
|
import { useKeyedChildListeners } from './useKeyedChildListeners';
|
|
46
47
|
import { useLazyValue } from './useLazyValue';
|
|
47
48
|
import { useNavigationHelpers } from './useNavigationHelpers';
|
|
@@ -58,9 +59,11 @@ PrivateValueStore;
|
|
|
58
59
|
|
|
59
60
|
type NavigatorRoute = {
|
|
60
61
|
key: string;
|
|
61
|
-
params?: NavigatorScreenParams<ParamListBase
|
|
62
|
+
params?: NavigatorScreenParams<ParamListBase> | undefined;
|
|
62
63
|
};
|
|
63
64
|
|
|
65
|
+
const CONSUMED_PARAMS = Symbol('CONSUMED_PARAMS');
|
|
66
|
+
|
|
64
67
|
const isScreen = (
|
|
65
68
|
child: React.ReactElement<unknown>
|
|
66
69
|
): child is React.ReactElement<{
|
|
@@ -324,6 +327,12 @@ export function useNavigationBuilder<
|
|
|
324
327
|
| NavigatorRoute
|
|
325
328
|
| undefined;
|
|
326
329
|
|
|
330
|
+
const isNestedParamsConsumed =
|
|
331
|
+
typeof route?.params === 'object' && route.params != null
|
|
332
|
+
? CONSUMED_PARAMS in route.params &&
|
|
333
|
+
route.params[CONSUMED_PARAMS] === route.params
|
|
334
|
+
: false;
|
|
335
|
+
|
|
327
336
|
const {
|
|
328
337
|
children,
|
|
329
338
|
layout,
|
|
@@ -418,7 +427,9 @@ export function useNavigationBuilder<
|
|
|
418
427
|
);
|
|
419
428
|
|
|
420
429
|
const isStateInitialized = React.useCallback(
|
|
421
|
-
|
|
430
|
+
<T extends NavigationState>(
|
|
431
|
+
state: T | PartialState<T> | undefined
|
|
432
|
+
): state is T =>
|
|
422
433
|
state !== undefined && state.stale === false && isStateValid(state),
|
|
423
434
|
[isStateValid]
|
|
424
435
|
);
|
|
@@ -438,14 +449,20 @@ export function useNavigationBuilder<
|
|
|
438
449
|
getIsInitial,
|
|
439
450
|
} = React.useContext(NavigationStateContext);
|
|
440
451
|
|
|
441
|
-
const
|
|
452
|
+
const stateCleanupRef = React.useRef<boolean>(false);
|
|
453
|
+
const lastStateRef = React.useRef<State | PartialState<State> | undefined>(
|
|
454
|
+
undefined
|
|
455
|
+
);
|
|
442
456
|
|
|
443
457
|
const setState = useLatestCallback(
|
|
444
|
-
(state:
|
|
445
|
-
if (
|
|
458
|
+
(state: State | PartialState<State> | undefined) => {
|
|
459
|
+
if (stateCleanupRef.current) {
|
|
460
|
+
// Store the state locally in case the current navigator is in `Activity`
|
|
461
|
+
lastStateRef.current = state;
|
|
462
|
+
|
|
446
463
|
// State might have been already cleaned up due to unmount
|
|
447
|
-
// We
|
|
448
|
-
//
|
|
464
|
+
// We don't want to update `route.state` in parent
|
|
465
|
+
// Otherwise it will be reused if a new navigator gets mounted
|
|
449
466
|
return;
|
|
450
467
|
}
|
|
451
468
|
|
|
@@ -457,11 +474,32 @@ export function useNavigationBuilder<
|
|
|
457
474
|
stateBeforeInitialization,
|
|
458
475
|
initializedState,
|
|
459
476
|
isFirstStateInitialization,
|
|
477
|
+
paramsUsedForInitialization,
|
|
460
478
|
] = React.useMemo((): [
|
|
461
479
|
PartialState<State> | undefined,
|
|
462
480
|
State | undefined,
|
|
463
481
|
boolean,
|
|
482
|
+
object | undefined,
|
|
464
483
|
] => {
|
|
484
|
+
// If the state was already cleaned up, but we have it stored in ref,
|
|
485
|
+
// It likely got cleaned up due to `<Activity mode="hidden">`
|
|
486
|
+
// We should reuse this state to avoid remounting screens
|
|
487
|
+
if (
|
|
488
|
+
stateCleanupRef.current &&
|
|
489
|
+
lastStateRef.current &&
|
|
490
|
+
isStateValid(lastStateRef.current)
|
|
491
|
+
) {
|
|
492
|
+
const state: State = isStateInitialized(lastStateRef.current)
|
|
493
|
+
? lastStateRef.current
|
|
494
|
+
: router.getRehydratedState(lastStateRef.current, {
|
|
495
|
+
routeNames,
|
|
496
|
+
routeParamList,
|
|
497
|
+
routeGetIdList,
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
return [undefined, state, false, undefined];
|
|
501
|
+
}
|
|
502
|
+
|
|
465
503
|
const initialRouteParamList = routeNames.reduce<
|
|
466
504
|
Record<string, object | undefined>
|
|
467
505
|
>((acc, curr) => {
|
|
@@ -494,7 +532,8 @@ export function useNavigationBuilder<
|
|
|
494
532
|
!(
|
|
495
533
|
typeof route?.params?.screen === 'string' &&
|
|
496
534
|
route?.params?.initial !== false
|
|
497
|
-
)
|
|
535
|
+
) &&
|
|
536
|
+
!isNestedParamsConsumed
|
|
498
537
|
) {
|
|
499
538
|
return [
|
|
500
539
|
undefined,
|
|
@@ -504,28 +543,40 @@ export function useNavigationBuilder<
|
|
|
504
543
|
routeGetIdList,
|
|
505
544
|
}),
|
|
506
545
|
true,
|
|
546
|
+
undefined,
|
|
507
547
|
];
|
|
508
548
|
} else {
|
|
509
|
-
const
|
|
510
|
-
const
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
549
|
+
const paramsForState = isNestedParamsConsumed ? undefined : route?.params;
|
|
550
|
+
const stateFromParams = paramsForState
|
|
551
|
+
? getStateFromParams(paramsForState)
|
|
552
|
+
: undefined;
|
|
553
|
+
|
|
554
|
+
const stateBeforeInitialization = (stateFromParams ?? currentState) as
|
|
555
|
+
| PartialState<State>
|
|
556
|
+
| undefined;
|
|
557
|
+
|
|
558
|
+
const hydratedState =
|
|
559
|
+
stateBeforeInitialization == null
|
|
560
|
+
? router.getInitialState({
|
|
561
|
+
routeNames,
|
|
562
|
+
routeParamList: initialRouteParamList,
|
|
563
|
+
routeGetIdList,
|
|
564
|
+
})
|
|
565
|
+
: router.getRehydratedState(stateBeforeInitialization, {
|
|
566
|
+
routeNames,
|
|
567
|
+
routeParamList: initialRouteParamList,
|
|
568
|
+
routeGetIdList,
|
|
569
|
+
});
|
|
520
570
|
|
|
521
571
|
if (
|
|
572
|
+
stateBeforeInitialization != null &&
|
|
522
573
|
options.routeNamesChangeBehavior === 'lastUnhandled' &&
|
|
523
574
|
doesStateHaveOnlyInvalidRoutes(stateBeforeInitialization)
|
|
524
575
|
) {
|
|
525
|
-
return [stateBeforeInitialization, hydratedState, true];
|
|
576
|
+
return [stateBeforeInitialization, hydratedState, true, paramsForState];
|
|
526
577
|
}
|
|
527
578
|
|
|
528
|
-
return [undefined, hydratedState, false];
|
|
579
|
+
return [undefined, hydratedState, false, paramsForState];
|
|
529
580
|
}
|
|
530
581
|
// We explicitly don't include routeNames, route.params etc. in the dep list
|
|
531
582
|
// below. We want to avoid forcing a new state to be calculated in those cases
|
|
@@ -603,22 +654,18 @@ export function useNavigationBuilder<
|
|
|
603
654
|
});
|
|
604
655
|
}
|
|
605
656
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
React.useEffect(() => {
|
|
609
|
-
previousNestedParamsRef.current = route?.params;
|
|
610
|
-
}, [route?.params]);
|
|
611
|
-
|
|
612
|
-
if (route?.params) {
|
|
613
|
-
const previousParams = previousNestedParamsRef.current;
|
|
657
|
+
let didConsumeNestedParams = route?.params === paramsUsedForInitialization;
|
|
614
658
|
|
|
659
|
+
if (route?.params && !didConsumeNestedParams) {
|
|
615
660
|
let action: CommonActions.Action | undefined;
|
|
616
661
|
|
|
617
662
|
if (
|
|
618
663
|
typeof route.params.state === 'object' &&
|
|
619
664
|
route.params.state != null &&
|
|
620
|
-
|
|
665
|
+
!isNestedParamsConsumed
|
|
621
666
|
) {
|
|
667
|
+
didConsumeNestedParams = true;
|
|
668
|
+
|
|
622
669
|
if (
|
|
623
670
|
options.routeNamesChangeBehavior === 'lastUnhandled' &&
|
|
624
671
|
doesStateHaveOnlyInvalidRoutes(route.params.state)
|
|
@@ -633,8 +680,10 @@ export function useNavigationBuilder<
|
|
|
633
680
|
} else if (
|
|
634
681
|
typeof route.params.screen === 'string' &&
|
|
635
682
|
((route.params.initial === false && isFirstStateInitialization) ||
|
|
636
|
-
|
|
683
|
+
!isNestedParamsConsumed)
|
|
637
684
|
) {
|
|
685
|
+
didConsumeNestedParams = true;
|
|
686
|
+
|
|
638
687
|
if (
|
|
639
688
|
options.routeNamesChangeBehavior === 'lastUnhandled' &&
|
|
640
689
|
!routeNames.includes(route.params.screen)
|
|
@@ -675,14 +724,26 @@ export function useNavigationBuilder<
|
|
|
675
724
|
: nextState;
|
|
676
725
|
}
|
|
677
726
|
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
727
|
+
React.useEffect(() => {
|
|
728
|
+
if (
|
|
729
|
+
didConsumeNestedParams &&
|
|
730
|
+
typeof route?.params === 'object' &&
|
|
731
|
+
route.params != null
|
|
732
|
+
) {
|
|
733
|
+
// Track whether the params have been already consumed
|
|
734
|
+
// Set it to the same object, so merged params can be handled again
|
|
735
|
+
Object.defineProperty(route.params, CONSUMED_PARAMS, {
|
|
736
|
+
value: route.params,
|
|
737
|
+
enumerable: false,
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
}, [didConsumeNestedParams, route?.params]);
|
|
741
|
+
|
|
742
|
+
const shouldUpdate = state !== nextState;
|
|
682
743
|
|
|
683
744
|
useScheduleUpdate(() => {
|
|
684
745
|
if (shouldUpdate) {
|
|
685
|
-
//
|
|
746
|
+
// Schedule an update if the state needs to be updated
|
|
686
747
|
setState(nextState);
|
|
687
748
|
|
|
688
749
|
if (shouldClearUnhandledState) {
|
|
@@ -696,25 +757,35 @@ export function useNavigationBuilder<
|
|
|
696
757
|
// So we override the state object we return to use the latest state as soon as possible
|
|
697
758
|
state = nextState;
|
|
698
759
|
|
|
760
|
+
// Last state to reuse if component gets cleaned up due to `<Activity mode="hidden">`
|
|
761
|
+
React.useEffect(() => {
|
|
762
|
+
lastStateRef.current = state;
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
const lastNotifiedStateRef = React.useRef<State | null>(null);
|
|
766
|
+
|
|
699
767
|
React.useEffect(() => {
|
|
700
768
|
// In strict mode, React will double-invoke effects.
|
|
701
769
|
// So we need to reset the flag if component was not unmounted
|
|
702
|
-
|
|
770
|
+
stateCleanupRef.current = false;
|
|
703
771
|
|
|
704
772
|
setKey(navigatorKey);
|
|
705
773
|
|
|
706
|
-
if (!getIsInitial()) {
|
|
774
|
+
if (!getIsInitial() && lastNotifiedStateRef.current !== state) {
|
|
707
775
|
// If it's not initial render, we need to update the state
|
|
708
776
|
// This will make sure that our container gets notifier of state changes due to new mounts
|
|
709
777
|
// This is necessary for proper screen tracking, URL updates etc.
|
|
710
|
-
|
|
778
|
+
// We only notify if the state is different what we already notified
|
|
779
|
+
// Otherwise this goes into a loop when inside `<Activity mode="hidden">`
|
|
780
|
+
setState(state);
|
|
781
|
+
lastNotifiedStateRef.current = state;
|
|
711
782
|
}
|
|
712
783
|
|
|
713
784
|
return () => {
|
|
714
785
|
// We need to clean up state for this navigator on unmount
|
|
715
786
|
if (getCurrentState() !== undefined && getKey() === navigatorKey) {
|
|
716
787
|
setCurrentState(undefined);
|
|
717
|
-
|
|
788
|
+
stateCleanupRef.current = true;
|
|
718
789
|
}
|
|
719
790
|
};
|
|
720
791
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
@@ -942,7 +1013,11 @@ export function useNavigationBuilder<
|
|
|
942
1013
|
<NavigationMetaContext.Provider value={undefined}>
|
|
943
1014
|
<NavigationHelpersContext.Provider value={navigation}>
|
|
944
1015
|
<NavigationStateListenerProvider state={state}>
|
|
945
|
-
<
|
|
1016
|
+
<FocusedRouteKeyContext.Provider
|
|
1017
|
+
value={state.routes[state.index].key}
|
|
1018
|
+
>
|
|
1019
|
+
<PreventRemoveProvider>{element}</PreventRemoveProvider>
|
|
1020
|
+
</FocusedRouteKeyContext.Provider>
|
|
946
1021
|
</NavigationStateListenerProvider>
|
|
947
1022
|
</NavigationHelpersContext.Provider>
|
|
948
1023
|
</NavigationMetaContext.Provider>
|
package/src/useOnAction.tsx
CHANGED
|
@@ -16,11 +16,11 @@ import type { EventMapCore } from './types';
|
|
|
16
16
|
import type { NavigationEventEmitter } from './useEventEmitter';
|
|
17
17
|
import { shouldPreventRemove, useOnPreventRemove } from './useOnPreventRemove';
|
|
18
18
|
|
|
19
|
-
type Options = {
|
|
20
|
-
router: Router<
|
|
21
|
-
key?: string;
|
|
22
|
-
getState: () =>
|
|
23
|
-
setState: (state:
|
|
19
|
+
type Options<State extends NavigationState> = {
|
|
20
|
+
router: Router<State, NavigationAction>;
|
|
21
|
+
key?: string | undefined;
|
|
22
|
+
getState: () => State;
|
|
23
|
+
setState: (state: State | PartialState<State>) => void;
|
|
24
24
|
actionListeners: ChildActionListener[];
|
|
25
25
|
beforeRemoveListeners: Record<string, ChildBeforeRemoveListener | undefined>;
|
|
26
26
|
routerConfigOptions: RouterConfigOptions;
|
|
@@ -36,7 +36,7 @@ type Options = {
|
|
|
36
36
|
*
|
|
37
37
|
* When the action handler handles as action, it returns `true`, otherwise `false`.
|
|
38
38
|
*/
|
|
39
|
-
export function useOnAction({
|
|
39
|
+
export function useOnAction<State extends NavigationState>({
|
|
40
40
|
router,
|
|
41
41
|
getState,
|
|
42
42
|
setState,
|
|
@@ -45,7 +45,7 @@ export function useOnAction({
|
|
|
45
45
|
beforeRemoveListeners,
|
|
46
46
|
routerConfigOptions,
|
|
47
47
|
emitter,
|
|
48
|
-
}: Options) {
|
|
48
|
+
}: Options<State>) {
|
|
49
49
|
const {
|
|
50
50
|
onAction: onActionParent,
|
|
51
51
|
onRouteFocus: onRouteFocusParent,
|
package/src/useOnRouteFocus.tsx
CHANGED
|
@@ -7,11 +7,11 @@ import * as React from 'react';
|
|
|
7
7
|
|
|
8
8
|
import { NavigationBuilderContext } from './NavigationBuilderContext';
|
|
9
9
|
|
|
10
|
-
type Options<Action extends NavigationAction> = {
|
|
11
|
-
router: Router<
|
|
12
|
-
getState: () =>
|
|
13
|
-
setState: (state:
|
|
14
|
-
key?: string;
|
|
10
|
+
type Options<State extends NavigationState, Action extends NavigationAction> = {
|
|
11
|
+
router: Router<State, Action>;
|
|
12
|
+
getState: () => State;
|
|
13
|
+
setState: (state: State) => void;
|
|
14
|
+
key?: string | undefined;
|
|
15
15
|
};
|
|
16
16
|
|
|
17
17
|
/**
|
|
@@ -19,12 +19,10 @@ type Options<Action extends NavigationAction> = {
|
|
|
19
19
|
* Focus action needs to be treated specially, coz when a nested route is focused,
|
|
20
20
|
* the parent navigators also needs to be focused.
|
|
21
21
|
*/
|
|
22
|
-
export function useOnRouteFocus<
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
setState,
|
|
27
|
-
}: Options<Action>) {
|
|
22
|
+
export function useOnRouteFocus<
|
|
23
|
+
State extends NavigationState,
|
|
24
|
+
Action extends NavigationAction,
|
|
25
|
+
>({ router, getState, key: sourceRouteKey, setState }: Options<State, Action>) {
|
|
28
26
|
const { onRouteFocus: onRouteFocusParent } = React.useContext(
|
|
29
27
|
NavigationBuilderContext
|
|
30
28
|
);
|
package/src/utilities.tsx
CHANGED
|
@@ -66,6 +66,24 @@ type ExtractSegmentParam<Segment extends string> =
|
|
|
66
66
|
? { [K in StripRegex<Param>]: string }
|
|
67
67
|
: {};
|
|
68
68
|
|
|
69
|
+
export type StandardSchemaValidationResult<Output> =
|
|
70
|
+
| { value: Output; issues?: undefined }
|
|
71
|
+
| { value?: undefined; issues: readonly unknown[] };
|
|
72
|
+
|
|
73
|
+
export type StandardSchemaV1<Input = unknown, Output = Input> = {
|
|
74
|
+
readonly '~standard': {
|
|
75
|
+
readonly version: 1;
|
|
76
|
+
readonly vendor: string;
|
|
77
|
+
readonly validate: (
|
|
78
|
+
value: Input
|
|
79
|
+
) =>
|
|
80
|
+
| StandardSchemaValidationResult<Output>
|
|
81
|
+
| Promise<StandardSchemaValidationResult<Output>>;
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export type QueryParamInput = string | string[] | null | undefined;
|
|
86
|
+
|
|
69
87
|
/**
|
|
70
88
|
* Extract path params from a path string.
|
|
71
89
|
* e.g. `/foo/:userId/:postId` -> `{ userId: string; postId: string }`
|
|
@@ -78,17 +96,67 @@ export type ExtractParamStrings<Path extends string> =
|
|
|
78
96
|
: ExtractSegmentParam<Path>;
|
|
79
97
|
|
|
80
98
|
/**
|
|
81
|
-
*
|
|
82
|
-
* Applies the return type of parse functions to the corresponding params.
|
|
99
|
+
* Get the type of params based on the `parse` config and the path pattern.
|
|
83
100
|
*/
|
|
84
101
|
export type ExtractParamsType<Params, Parse> = {
|
|
102
|
+
/**
|
|
103
|
+
* Base param types from path pattern
|
|
104
|
+
* Refine the type based on standard schema, then parse function
|
|
105
|
+
* Otherwise, use the type from path pattern
|
|
106
|
+
*/
|
|
85
107
|
[K in keyof Params]: K extends keyof Parse
|
|
86
|
-
? Parse[K] extends
|
|
108
|
+
? Parse[K] extends StandardSchemaV1<unknown, infer R>
|
|
87
109
|
? R
|
|
88
|
-
:
|
|
110
|
+
: Parse[K] extends (value: string) => infer R
|
|
111
|
+
? R
|
|
112
|
+
: Params[K]
|
|
89
113
|
: Params[K];
|
|
114
|
+
} & {
|
|
115
|
+
/**
|
|
116
|
+
* Optional param types not in path pattern (for query params)
|
|
117
|
+
*/
|
|
118
|
+
[K in QueryParamOptionalKeys<Params, Parse>]?: QueryParamValue<Parse[K]>;
|
|
119
|
+
} & {
|
|
120
|
+
/**
|
|
121
|
+
* Required param types not in path pattern (for query params)
|
|
122
|
+
*/
|
|
123
|
+
[K in QueryParamRequiredKeys<Params, Parse>]: QueryParamValue<Parse[K]>;
|
|
90
124
|
};
|
|
91
125
|
|
|
126
|
+
type QueryParamValue<ParseValue> =
|
|
127
|
+
ParseValue extends StandardSchemaV1<unknown, infer R>
|
|
128
|
+
? R
|
|
129
|
+
: ParseValue extends (value: string) => infer R
|
|
130
|
+
? R
|
|
131
|
+
: never;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* For schema, it's optional if the output has `undefined`, otherwise required
|
|
135
|
+
* For parse function, it's always optional as it can't say if it's required or not
|
|
136
|
+
*/
|
|
137
|
+
type QueryParamOptionalKeys<Params, Parse> = {
|
|
138
|
+
[K in Exclude<keyof Parse, keyof Params>]: Parse[K] extends StandardSchemaV1<
|
|
139
|
+
unknown,
|
|
140
|
+
infer R
|
|
141
|
+
>
|
|
142
|
+
? undefined extends R
|
|
143
|
+
? K
|
|
144
|
+
: never
|
|
145
|
+
: Parse[K] extends (value: string) => unknown
|
|
146
|
+
? K
|
|
147
|
+
: never;
|
|
148
|
+
}[Exclude<keyof Parse, keyof Params>];
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Exclude optional keys to get required keys
|
|
152
|
+
* For schema, it's required if the output doesn't have `undefined`, otherwise optional
|
|
153
|
+
* It doesn't include parse functions (as they are always optional)
|
|
154
|
+
*/
|
|
155
|
+
type QueryParamRequiredKeys<Params, Parse> = Exclude<
|
|
156
|
+
Exclude<keyof Parse, keyof Params>,
|
|
157
|
+
QueryParamOptionalKeys<Params, Parse>
|
|
158
|
+
>;
|
|
159
|
+
|
|
92
160
|
/**
|
|
93
161
|
* Infer the path string from a linking config.
|
|
94
162
|
*/
|