Legend:
- Unmodified
- Added
- Removed
-
imaps-frontend/node_modules/@remix-run/router/router.ts
rd565449 r0c6b92a 23 23 FormMethod, 24 24 HTMLFormMethod, 25 HandlerResult,25 DataStrategyResult, 26 26 ImmutableRouteKey, 27 27 MapRoutePropertiesFunction, … … 36 36 V7_FormMethod, 37 37 V7_MutationFormMethod, 38 AgnosticPatchRoutesOn MissFunction,38 AgnosticPatchRoutesOnNavigationFunction, 39 39 DataWithResponseInit, 40 40 } from "./utils"; … … 392 392 hydrationData?: HydrationState; 393 393 window?: Window; 394 unstable_patchRoutesOnMiss?: AgnosticPatchRoutesOnMissFunction;395 unstable_dataStrategy?: DataStrategyFunction;394 dataStrategy?: DataStrategyFunction; 395 patchRoutesOnNavigation?: AgnosticPatchRoutesOnNavigationFunction; 396 396 } 397 397 … … 423 423 requestContext?: unknown; 424 424 skipLoaderErrorBubbling?: boolean; 425 unstable_dataStrategy?: DataStrategyFunction;425 dataStrategy?: DataStrategyFunction; 426 426 } 427 427 ): Promise<StaticHandlerContext | Response>; … … 431 431 routeId?: string; 432 432 requestContext?: unknown; 433 unstable_dataStrategy?: DataStrategyFunction;433 dataStrategy?: DataStrategyFunction; 434 434 } 435 435 ): Promise<any>; … … 449 449 opts: { 450 450 deletedFetchers: string[]; 451 unstable_viewTransitionOpts?: ViewTransitionOpts;452 unstable_flushSync: boolean;451 viewTransitionOpts?: ViewTransitionOpts; 452 flushSync: boolean; 453 453 } 454 454 ): void; … … 476 476 preventScrollReset?: boolean; 477 477 relative?: RelativeRoutingType; 478 unstable_flushSync?: boolean;478 flushSync?: boolean; 479 479 }; 480 480 … … 484 484 state?: any; 485 485 fromRouteId?: string; 486 unstable_viewTransition?: boolean;486 viewTransition?: boolean; 487 487 }; 488 488 … … 798 798 let inFlightDataRoutes: AgnosticDataRouteObject[] | undefined; 799 799 let basename = init.basename || "/"; 800 let dataStrategyImpl = init. unstable_dataStrategy || defaultDataStrategy;801 let patchRoutesOn MissImpl = init.unstable_patchRoutesOnMiss;800 let dataStrategyImpl = init.dataStrategy || defaultDataStrategy; 801 let patchRoutesOnNavigationImpl = init.patchRoutesOnNavigation; 802 802 803 803 // Config driven behavior flags … … 832 832 let initialErrors: RouteData | null = null; 833 833 834 if (initialMatches == null && !patchRoutesOn MissImpl) {834 if (initialMatches == null && !patchRoutesOnNavigationImpl) { 835 835 // If we do not match a user-provided-route, fall back to the root 836 836 // to allow the error boundary to take over … … 843 843 } 844 844 845 // In SPA apps, if the user provided a patchRoutesOn Missimplementation and845 // In SPA apps, if the user provided a patchRoutesOnNavigation implementation and 846 846 // our initial match is a splat route, clear them out so we run through lazy 847 847 // discovery on hydration in case there's a more accurate lazy route match. … … 866 866 867 867 // If partial hydration and fog of war is enabled, we will be running 868 // `patchRoutesOn Miss` during hydration so include any partial matches as868 // `patchRoutesOnNavigation` during hydration so include any partial matches as 869 869 // the initial matches so we can properly render `HydrateFallback`'s 870 870 if (future.v7_partialHydration) { … … 891 891 let loaderData = init.hydrationData ? init.hydrationData.loaderData : null; 892 892 let errors = init.hydrationData ? init.hydrationData.errors : null; 893 let isRouteInitialized = (m: AgnosticDataRouteMatch) => {894 // No loader, nothing to initialize895 if (!m.route.loader) {896 return true;897 }898 // Explicitly opting-in to running on hydration899 if (900 typeof m.route.loader === "function" &&901 m.route.loader.hydrate === true902 ) {903 return false;904 }905 // Otherwise, initialized if hydrated with data or an error906 return (907 (loaderData && loaderData[m.route.id] !== undefined) ||908 (errors && errors[m.route.id] !== undefined)909 );910 };911 912 893 // If errors exist, don't consider routes below the boundary 913 894 if (errors) { … … 915 896 (m) => errors![m.route.id] !== undefined 916 897 ); 917 initialized = initialMatches.slice(0, idx + 1).every(isRouteInitialized); 898 initialized = initialMatches 899 .slice(0, idx + 1) 900 .every((m) => !shouldLoadRouteOnHydration(m.route, loaderData, errors)); 918 901 } else { 919 initialized = initialMatches.every(isRouteInitialized); 902 initialized = initialMatches.every( 903 (m) => !shouldLoadRouteOnHydration(m.route, loaderData, errors) 904 ); 920 905 } 921 906 } else { … … 1021 1006 let blockerFunctions = new Map<string, BlockerFunction>(); 1022 1007 1023 // Map of pending patchRoutesOn Miss() promises (keyed by path/matches) so1008 // Map of pending patchRoutesOnNavigation() promises (keyed by path/matches) so 1024 1009 // that we only kick them off once for a given combo 1025 1010 let pendingPatchRoutes = new Map< 1026 1011 string, 1027 ReturnType<AgnosticPatchRoutesOn MissFunction>1012 ReturnType<AgnosticPatchRoutesOnNavigationFunction> 1028 1013 >(); 1029 1014 1030 1015 // Flag to ignore the next history update, so we can revert the URL change on 1031 1016 // a POP navigation that was blocked by the user without touching router state 1032 let ignoreNextHistoryUpdate = false;1017 let unblockBlockerHistoryUpdate: (() => void) | undefined = undefined; 1033 1018 1034 1019 // Initialize the router, all side effects should be kicked off from here. … … 1042 1027 // Ignore this event if it was just us resetting the URL from a 1043 1028 // blocked POP navigation 1044 if (ignoreNextHistoryUpdate) { 1045 ignoreNextHistoryUpdate = false; 1029 if (unblockBlockerHistoryUpdate) { 1030 unblockBlockerHistoryUpdate(); 1031 unblockBlockerHistoryUpdate = undefined; 1046 1032 return; 1047 1033 } … … 1065 1051 if (blockerKey && delta != null) { 1066 1052 // Restore the URL to match the current UI, but don't update router state 1067 ignoreNextHistoryUpdate = true; 1053 let nextHistoryUpdatePromise = new Promise<void>((resolve) => { 1054 unblockBlockerHistoryUpdate = resolve; 1055 }); 1068 1056 init.history.go(delta * -1); 1069 1057 … … 1079 1067 location, 1080 1068 }); 1081 // Re-do the same POP navigation we just blocked 1082 init.history.go(delta); 1069 // Re-do the same POP navigation we just blocked, after the url 1070 // restoration is also complete. See: 1071 // https://github.com/remix-run/react-router/issues/11613 1072 nextHistoryUpdatePromise.then(() => init.history.go(delta)); 1083 1073 }, 1084 1074 reset() { … … 1179 1169 subscriber(state, { 1180 1170 deletedFetchers: deletedFetchersKeys, 1181 unstable_viewTransitionOpts: opts.viewTransitionOpts,1182 unstable_flushSync: opts.flushSync === true,1171 viewTransitionOpts: opts.viewTransitionOpts, 1172 flushSync: opts.flushSync === true, 1183 1173 }) 1184 1174 ); … … 1403 1393 : undefined; 1404 1394 1405 let flushSync = (opts && opts. unstable_flushSync) === true;1395 let flushSync = (opts && opts.flushSync) === true; 1406 1396 1407 1397 let blockerKey = shouldBlockNavigation({ … … 1442 1432 preventScrollReset, 1443 1433 replace: opts && opts.replace, 1444 enableViewTransition: opts && opts. unstable_viewTransition,1434 enableViewTransition: opts && opts.viewTransition, 1445 1435 flushSync, 1446 1436 }); … … 1476 1466 pendingAction || state.historyAction, 1477 1467 state.navigation.location, 1478 { overrideNavigation: state.navigation } 1468 { 1469 overrideNavigation: state.navigation, 1470 // Proxy through any rending view transition 1471 enableViewTransition: pendingViewTransitionEnabled === true, 1472 } 1479 1473 ); 1480 1474 } … … 1547 1541 // mutation submission. 1548 1542 // 1549 // Ignore on initial page loads because since the initial loadwill always1543 // Ignore on initial page loads because since the initial hydration will always 1550 1544 // be "same hash". For example, on /page#hash and submit a <Form method="post"> 1551 1545 // which will default to a navigation to /page … … 1697 1691 return { shortCircuited: true }; 1698 1692 } else if (discoverResult.type === "error") { 1699 let { boundaryId, error } = handleDiscoverRouteError( 1700 location.pathname, 1701 discoverResult 1702 ); 1693 let boundaryId = findNearestBoundary(discoverResult.partialMatches) 1694 .route.id; 1703 1695 return { 1704 1696 matches: discoverResult.partialMatches, … … 1707 1699 { 1708 1700 type: ResultType.error, 1709 error ,1701 error: discoverResult.error, 1710 1702 }, 1711 1703 ], … … 1746 1738 let results = await callDataStrategy( 1747 1739 "action", 1740 state, 1748 1741 request, 1749 1742 [actionMatch], 1750 matches 1743 matches, 1744 null 1751 1745 ); 1752 result = results[ 0];1746 result = results[actionMatch.route.id]; 1753 1747 1754 1748 if (request.signal.aborted) { … … 1772 1766 replace = location === state.location.pathname + state.location.search; 1773 1767 } 1774 await startRedirectNavigation(request, result, {1768 await startRedirectNavigation(request, result, true, { 1775 1769 submission, 1776 1770 replace, … … 1873 1867 return { shortCircuited: true }; 1874 1868 } else if (discoverResult.type === "error") { 1875 let { boundaryId, error } = handleDiscoverRouteError( 1876 location.pathname, 1877 discoverResult 1878 ); 1869 let boundaryId = findNearestBoundary(discoverResult.partialMatches) 1870 .route.id; 1879 1871 return { 1880 1872 matches: discoverResult.partialMatches, 1881 1873 loaderData: {}, 1882 1874 errors: { 1883 [boundaryId]: error,1875 [boundaryId]: discoverResult.error, 1884 1876 }, 1885 1877 }; … … 1969 1961 1970 1962 revalidatingFetchers.forEach((rf) => { 1971 if (fetchControllers.has(rf.key)) { 1972 abortFetcher(rf.key); 1973 } 1963 abortFetcher(rf.key); 1974 1964 if (rf.controller) { 1975 1965 // Fetchers use an independent AbortController so that aborting a fetcher … … 1992 1982 let { loaderResults, fetcherResults } = 1993 1983 await callLoadersAndMaybeResolveData( 1994 state .matches,1984 state, 1995 1985 matches, 1996 1986 matchesToLoad, … … 2012 2002 ); 2013 2003 } 2004 2014 2005 revalidatingFetchers.forEach((rf) => fetchControllers.delete(rf.key)); 2015 2006 2016 2007 // If any loaders returned a redirect Response, start a new REPLACE navigation 2017 let redirect = findRedirect( [...loaderResults, ...fetcherResults]);2008 let redirect = findRedirect(loaderResults); 2018 2009 if (redirect) { 2019 if (redirect.idx >= matchesToLoad.length) { 2020 // If this redirect came from a fetcher make sure we mark it in 2021 // fetchRedirectIds so it doesn't get revalidated on the next set of 2022 // loader executions 2023 let fetcherKey = 2024 revalidatingFetchers[redirect.idx - matchesToLoad.length].key; 2025 fetchRedirectIds.add(fetcherKey); 2026 } 2027 await startRedirectNavigation(request, redirect.result, { 2010 await startRedirectNavigation(request, redirect.result, true, { 2011 replace, 2012 }); 2013 return { shortCircuited: true }; 2014 } 2015 2016 redirect = findRedirect(fetcherResults); 2017 if (redirect) { 2018 // If this redirect came from a fetcher make sure we mark it in 2019 // fetchRedirectIds so it doesn't get revalidated on the next set of 2020 // loader executions 2021 fetchRedirectIds.add(redirect.key); 2022 await startRedirectNavigation(request, redirect.result, true, { 2028 2023 replace, 2029 2024 }); … … 2035 2030 state, 2036 2031 matches, 2037 matchesToLoad,2038 2032 loaderResults, 2039 2033 pendingActionResult, … … 2055 2049 }); 2056 2050 2057 // During partial hydration, preserve SSR errors for routes that don't re-run2051 // Preserve SSR errors during partial hydration 2058 2052 if (future.v7_partialHydration && initialHydration && state.errors) { 2059 Object.entries(state.errors) 2060 .filter(([id]) => !matchesToLoad.some((m) => m.route.id === id)) 2061 .forEach(([routeId, error]) => { 2062 errors = Object.assign(errors || {}, { [routeId]: error }); 2063 }); 2053 errors = { ...state.errors, ...errors }; 2064 2054 } 2065 2055 … … 2125 2115 } 2126 2116 2127 if (fetchControllers.has(key)) abortFetcher(key); 2128 let flushSync = (opts && opts.unstable_flushSync) === true; 2117 abortFetcher(key); 2118 2119 let flushSync = (opts && opts.flushSync) === true; 2129 2120 2130 2121 let routesToUse = inFlightDataRoutes || dataRoutes; … … 2170 2161 let match = getTargetMatch(matches, path); 2171 2162 2172 pendingPreventScrollReset = (opts && opts.preventScrollReset) === true;2163 let preventScrollReset = (opts && opts.preventScrollReset) === true; 2173 2164 2174 2165 if (submission && isMutationMethod(submission.formMethod)) { … … 2181 2172 fogOfWar.active, 2182 2173 flushSync, 2174 preventScrollReset, 2183 2175 submission 2184 2176 ); … … 2197 2189 fogOfWar.active, 2198 2190 flushSync, 2191 preventScrollReset, 2199 2192 submission 2200 2193 ); … … 2211 2204 isFogOfWar: boolean, 2212 2205 flushSync: boolean, 2206 preventScrollReset: boolean, 2213 2207 submission: Submission 2214 2208 ) { … … 2257 2251 return; 2258 2252 } else if (discoverResult.type === "error") { 2259 let { error } = handleDiscoverRouteError(path, discoverResult); 2260 setFetcherError(key, routeId, error, { flushSync }); 2253 setFetcherError(key, routeId, discoverResult.error, { flushSync }); 2261 2254 return; 2262 2255 } else if (!discoverResult.matches) { … … 2284 2277 let actionResults = await callDataStrategy( 2285 2278 "action", 2279 state, 2286 2280 fetchRequest, 2287 2281 [match], 2288 requestMatches 2282 requestMatches, 2283 key 2289 2284 ); 2290 let actionResult = actionResults[ 0];2285 let actionResult = actionResults[match.route.id]; 2291 2286 2292 2287 if (fetchRequest.signal.aborted) { … … 2321 2316 fetchRedirectIds.add(key); 2322 2317 updateFetcherState(key, getLoadingFetcher(submission)); 2323 return startRedirectNavigation(fetchRequest, actionResult, {2318 return startRedirectNavigation(fetchRequest, actionResult, false, { 2324 2319 fetcherSubmission: submission, 2320 preventScrollReset, 2325 2321 }); 2326 2322 } … … 2392 2388 ); 2393 2389 state.fetchers.set(staleKey, revalidatingFetcher); 2394 if (fetchControllers.has(staleKey)) { 2395 abortFetcher(staleKey); 2396 } 2390 abortFetcher(staleKey); 2397 2391 if (rf.controller) { 2398 2392 fetchControllers.set(staleKey, rf.controller); … … 2412 2406 let { loaderResults, fetcherResults } = 2413 2407 await callLoadersAndMaybeResolveData( 2414 state .matches,2408 state, 2415 2409 matches, 2416 2410 matchesToLoad, … … 2432 2426 revalidatingFetchers.forEach((r) => fetchControllers.delete(r.key)); 2433 2427 2434 let redirect = findRedirect( [...loaderResults, ...fetcherResults]);2428 let redirect = findRedirect(loaderResults); 2435 2429 if (redirect) { 2436 if (redirect.idx >= matchesToLoad.length) { 2437 // If this redirect came from a fetcher make sure we mark it in 2438 // fetchRedirectIds so it doesn't get revalidated on the next set of 2439 // loader executions 2440 let fetcherKey = 2441 revalidatingFetchers[redirect.idx - matchesToLoad.length].key; 2442 fetchRedirectIds.add(fetcherKey); 2443 } 2444 return startRedirectNavigation(revalidationRequest, redirect.result); 2430 return startRedirectNavigation( 2431 revalidationRequest, 2432 redirect.result, 2433 false, 2434 { preventScrollReset } 2435 ); 2436 } 2437 2438 redirect = findRedirect(fetcherResults); 2439 if (redirect) { 2440 // If this redirect came from a fetcher make sure we mark it in 2441 // fetchRedirectIds so it doesn't get revalidated on the next set of 2442 // loader executions 2443 fetchRedirectIds.add(redirect.key); 2444 return startRedirectNavigation( 2445 revalidationRequest, 2446 redirect.result, 2447 false, 2448 { preventScrollReset } 2449 ); 2445 2450 } 2446 2451 … … 2448 2453 let { loaderData, errors } = processLoaderData( 2449 2454 state, 2450 state.matches, 2451 matchesToLoad, 2455 matches, 2452 2456 loaderResults, 2453 2457 undefined, … … 2509 2513 isFogOfWar: boolean, 2510 2514 flushSync: boolean, 2515 preventScrollReset: boolean, 2511 2516 submission?: Submission 2512 2517 ) { … … 2538 2543 return; 2539 2544 } else if (discoverResult.type === "error") { 2540 let { error } = handleDiscoverRouteError(path, discoverResult); 2541 setFetcherError(key, routeId, error, { flushSync }); 2545 setFetcherError(key, routeId, discoverResult.error, { flushSync }); 2542 2546 return; 2543 2547 } else if (!discoverResult.matches) { … … 2561 2565 let results = await callDataStrategy( 2562 2566 "loader", 2567 state, 2563 2568 fetchRequest, 2564 2569 [match], 2565 matches 2570 matches, 2571 key 2566 2572 ); 2567 let result = results[ 0];2573 let result = results[match.route.id]; 2568 2574 2569 2575 // Deferred isn't supported for fetcher loads, await everything and treat it … … 2603 2609 } else { 2604 2610 fetchRedirectIds.add(key); 2605 await startRedirectNavigation(fetchRequest, result); 2611 await startRedirectNavigation(fetchRequest, result, false, { 2612 preventScrollReset, 2613 }); 2606 2614 return; 2607 2615 } … … 2642 2650 request: Request, 2643 2651 redirect: RedirectResult, 2652 isNavigation: boolean, 2644 2653 { 2645 2654 submission, 2646 2655 fetcherSubmission, 2656 preventScrollReset, 2647 2657 replace, 2648 2658 }: { 2649 2659 submission?: Submission; 2650 2660 fetcherSubmission?: Submission; 2661 preventScrollReset?: boolean; 2651 2662 replace?: boolean; 2652 2663 } = {} … … 2728 2739 formAction: location, 2729 2740 }, 2730 // Preserve this flag across redirects 2731 preventScrollReset: pendingPreventScrollReset, 2741 // Preserve these flags across redirects 2742 preventScrollReset: preventScrollReset || pendingPreventScrollReset, 2743 enableViewTransition: isNavigation 2744 ? pendingViewTransitionEnabled 2745 : undefined, 2732 2746 }); 2733 2747 } else { … … 2742 2756 // Send fetcher submissions through for shouldRevalidate 2743 2757 fetcherSubmission, 2744 // Preserve this flag across redirects 2745 preventScrollReset: pendingPreventScrollReset, 2758 // Preserve these flags across redirects 2759 preventScrollReset: preventScrollReset || pendingPreventScrollReset, 2760 enableViewTransition: isNavigation 2761 ? pendingViewTransitionEnabled 2762 : undefined, 2746 2763 }); 2747 2764 } … … 2752 2769 async function callDataStrategy( 2753 2770 type: "loader" | "action", 2771 state: RouterState, 2754 2772 request: Request, 2755 2773 matchesToLoad: AgnosticDataRouteMatch[], 2756 matches: AgnosticDataRouteMatch[] 2757 ): Promise<DataResult[]> { 2774 matches: AgnosticDataRouteMatch[], 2775 fetcherKey: string | null 2776 ): Promise<Record<string, DataResult>> { 2777 let results: Record<string, DataStrategyResult>; 2778 let dataResults: Record<string, DataResult> = {}; 2758 2779 try { 2759 letresults = await callDataStrategyImpl(2780 results = await callDataStrategyImpl( 2760 2781 dataStrategyImpl, 2761 2782 type, 2783 state, 2762 2784 request, 2763 2785 matchesToLoad, 2764 2786 matches, 2787 fetcherKey, 2765 2788 manifest, 2766 2789 mapRouteProperties 2767 );2768 2769 return await Promise.all(2770 results.map((result, i) => {2771 if (isRedirectHandlerResult(result)) {2772 let response = result.result as Response;2773 return {2774 type: ResultType.redirect,2775 response: normalizeRelativeRoutingRedirectResponse(2776 response,2777 request,2778 matchesToLoad[i].route.id,2779 matches,2780 basename,2781 future.v7_relativeSplatPath2782 ),2783 };2784 }2785 2786 return convertHandlerResultToDataResult(result);2787 })2788 2790 ); 2789 2791 } catch (e) { 2790 2792 // If the outer dataStrategy method throws, just return the error for all 2791 2793 // matches - and it'll naturally bubble to the root 2792 return matchesToLoad.map(() => ({ 2793 type: ResultType.error, 2794 error: e, 2795 })); 2796 } 2794 matchesToLoad.forEach((m) => { 2795 dataResults[m.route.id] = { 2796 type: ResultType.error, 2797 error: e, 2798 }; 2799 }); 2800 return dataResults; 2801 } 2802 2803 for (let [routeId, result] of Object.entries(results)) { 2804 if (isRedirectDataStrategyResultResult(result)) { 2805 let response = result.result as Response; 2806 dataResults[routeId] = { 2807 type: ResultType.redirect, 2808 response: normalizeRelativeRoutingRedirectResponse( 2809 response, 2810 request, 2811 routeId, 2812 matches, 2813 basename, 2814 future.v7_relativeSplatPath 2815 ), 2816 }; 2817 } else { 2818 dataResults[routeId] = await convertDataStrategyResultToDataResult( 2819 result 2820 ); 2821 } 2822 } 2823 2824 return dataResults; 2797 2825 } 2798 2826 2799 2827 async function callLoadersAndMaybeResolveData( 2800 currentMatches: AgnosticDataRouteMatch[],2828 state: RouterState, 2801 2829 matches: AgnosticDataRouteMatch[], 2802 2830 matchesToLoad: AgnosticDataRouteMatch[], … … 2804 2832 request: Request 2805 2833 ) { 2806 let [loaderResults, ...fetcherResults] = await Promise.all([ 2807 matchesToLoad.length 2808 ? callDataStrategy("loader", request, matchesToLoad, matches) 2809 : [], 2810 ...fetchersToLoad.map((f) => { 2834 let currentMatches = state.matches; 2835 2836 // Kick off loaders and fetchers in parallel 2837 let loaderResultsPromise = callDataStrategy( 2838 "loader", 2839 state, 2840 request, 2841 matchesToLoad, 2842 matches, 2843 null 2844 ); 2845 2846 let fetcherResultsPromise = Promise.all( 2847 fetchersToLoad.map(async (f) => { 2811 2848 if (f.matches && f.match && f.controller) { 2812 let fetcherRequest = createClientSideRequest( 2813 init.history, 2814 f.path, 2815 f.controller.signal 2849 let results = await callDataStrategy( 2850 "loader", 2851 state, 2852 createClientSideRequest(init.history, f.path, f.controller.signal), 2853 [f.match], 2854 f.matches, 2855 f.key 2816 2856 ); 2817 return callDataStrategy( 2818 "loader", 2819 fetcherRequest, 2820 [f.match], 2821 f.matches 2822 ).then((r) => r[0]); 2857 let result = results[f.match.route.id]; 2858 // Fetcher results are keyed by fetcher key from here on out, not routeId 2859 return { [f.key]: result }; 2823 2860 } else { 2824 return Promise.resolve<DataResult>({ 2825 type: ResultType.error, 2826 error: getInternalRouterError(404, { 2827 pathname: f.path, 2828 }), 2861 return Promise.resolve({ 2862 [f.key]: { 2863 type: ResultType.error, 2864 error: getInternalRouterError(404, { 2865 pathname: f.path, 2866 }), 2867 } as ErrorResult, 2829 2868 }); 2830 2869 } 2831 }), 2832 ]); 2870 }) 2871 ); 2872 2873 let loaderResults = await loaderResultsPromise; 2874 let fetcherResults = (await fetcherResultsPromise).reduce( 2875 (acc, r) => Object.assign(acc, r), 2876 {} 2877 ); 2833 2878 2834 2879 await Promise.all([ 2835 resolveDeferredResults( 2880 resolveNavigationDeferredResults( 2881 matches, 2882 loaderResults, 2883 request.signal, 2836 2884 currentMatches, 2837 matchesToLoad,2838 loaderResults,2839 loaderResults.map(() => request.signal),2840 false,2841 2885 state.loaderData 2842 2886 ), 2843 resolveDeferredResults( 2844 currentMatches, 2845 fetchersToLoad.map((f) => f.match), 2846 fetcherResults, 2847 fetchersToLoad.map((f) => (f.controller ? f.controller.signal : null)), 2848 true 2849 ), 2887 resolveFetcherDeferredResults(matches, fetcherResults, fetchersToLoad), 2850 2888 ]); 2851 2889 … … 2868 2906 if (fetchControllers.has(key)) { 2869 2907 cancelledFetcherLoads.add(key); 2870 abortFetcher(key);2871 }2908 } 2909 abortFetcher(key); 2872 2910 }); 2873 2911 } … … 2952 2990 function abortFetcher(key: string) { 2953 2991 let controller = fetchControllers.get(key); 2954 invariant(controller, `Expected fetch controller: ${key}`); 2955 controller.abort(); 2956 fetchControllers.delete(key); 2992 if (controller) { 2993 controller.abort(); 2994 fetchControllers.delete(key); 2995 } 2957 2996 } 2958 2997 … … 3078 3117 3079 3118 return { notFoundMatches: matches, route, error }; 3080 }3081 3082 function handleDiscoverRouteError(3083 pathname: string,3084 discoverResult: DiscoverRoutesErrorResult3085 ) {3086 return {3087 boundaryId: findNearestBoundary(discoverResult.partialMatches).route.id,3088 error: getInternalRouterError(400, {3089 type: "route-discovery",3090 pathname,3091 message:3092 discoverResult.error != null && "message" in discoverResult.error3093 ? discoverResult.error3094 : String(discoverResult.error),3095 }),3096 };3097 3119 } 3098 3120 … … 3183 3205 pathname: string 3184 3206 ): { active: boolean; matches: AgnosticDataRouteMatch[] | null } { 3185 if (patchRoutesOn MissImpl) {3207 if (patchRoutesOnNavigationImpl) { 3186 3208 if (!matches) { 3187 3209 let fogMatches = matchRoutesImpl<AgnosticDataRouteObject>( … … 3194 3216 return { active: true, matches: fogMatches || [] }; 3195 3217 } else { 3196 let leafRoute = matches[matches.length - 1].route; 3197 if ( 3198 leafRoute.path && 3199 (leafRoute.path === "*" || leafRoute.path.endsWith("/*")) 3200 ) { 3201 // If we matched a splat, it might only be because we haven't yet fetched 3202 // the children that would match with a higher score, so let's fetch 3203 // around and find out 3218 if (Object.keys(matches[0].params).length > 0) { 3219 // If we matched a dynamic param or a splat, it might only be because 3220 // we haven't yet discovered other routes that would match with a 3221 // higher score. Call patchRoutesOnNavigation just to be sure 3204 3222 let partialMatches = matchRoutesImpl<AgnosticDataRouteObject>( 3205 3223 routesToUse, … … 3236 3254 signal: AbortSignal 3237 3255 ): Promise<DiscoverRoutesResult> { 3256 if (!patchRoutesOnNavigationImpl) { 3257 return { type: "success", matches }; 3258 } 3259 3238 3260 let partialMatches: AgnosticDataRouteMatch[] | null = matches; 3239 let route =3240 partialMatches.length > 03241 ? partialMatches[partialMatches.length - 1].route3242 : null;3243 3261 while (true) { 3244 3262 let isNonHMR = inFlightDataRoutes == null; 3245 3263 let routesToUse = inFlightDataRoutes || dataRoutes; 3264 let localManifest = manifest; 3246 3265 try { 3247 await loadLazyRouteChildren( 3248 patchRoutesOnMissImpl!, 3249 pathname, 3250 partialMatches, 3251 routesToUse, 3252 manifest, 3253 mapRouteProperties, 3254 pendingPatchRoutes, 3255 signal 3256 ); 3266 await patchRoutesOnNavigationImpl({ 3267 path: pathname, 3268 matches: partialMatches, 3269 patch: (routeId, children) => { 3270 if (signal.aborted) return; 3271 patchRoutesImpl( 3272 routeId, 3273 children, 3274 routesToUse, 3275 localManifest, 3276 mapRouteProperties 3277 ); 3278 }, 3279 }); 3257 3280 } catch (e) { 3258 3281 return { type: "error", error: e, partialMatches }; … … 3264 3287 // HMR will already update the identity and reflow when it lands 3265 3288 // `inFlightDataRoutes` in `completeNavigation` 3266 if (isNonHMR ) {3289 if (isNonHMR && !signal.aborted) { 3267 3290 dataRoutes = [...dataRoutes]; 3268 3291 } … … 3274 3297 3275 3298 let newMatches = matchRoutes(routesToUse, pathname, basename); 3276 let matchedSplat = false;3277 3299 if (newMatches) { 3278 let leafRoute = newMatches[newMatches.length - 1].route; 3279 3280 if (leafRoute.index) { 3281 // If we found an index route, we can stop 3282 return { type: "success", matches: newMatches }; 3283 } 3284 3285 if (leafRoute.path && leafRoute.path.length > 0) { 3286 if (leafRoute.path === "*") { 3287 // If we found a splat route, we can't be sure there's not a 3288 // higher-scoring route down some partial matches trail so we need 3289 // to check that out 3290 matchedSplat = true; 3291 } else { 3292 // If we found a non-splat route, we can stop 3293 return { type: "success", matches: newMatches }; 3294 } 3295 } 3300 return { type: "success", matches: newMatches }; 3296 3301 } 3297 3302 … … 3303 3308 ); 3304 3309 3305 // If we are no longer partially matching anything, this was either a 3306 // legit splat match above, or it's a 404. Also avoid loops if the 3307 // second pass results in the same partial matches 3310 // Avoid loops if the second pass results in the same partial matches 3308 3311 if ( 3309 3312 !newPartialMatches || 3310 partialMatches.map((m) => m.route.id).join("-") === 3311 newPartialMatches.map((m) => m.route.id).join("-") 3313 (partialMatches.length === newPartialMatches.length && 3314 partialMatches.every( 3315 (m, i) => m.route.id === newPartialMatches![i].route.id 3316 )) 3312 3317 ) { 3313 return { type: "success", matches: matchedSplat ? newMatches :null };3318 return { type: "success", matches: null }; 3314 3319 } 3315 3320 3316 3321 partialMatches = newPartialMatches; 3317 route = partialMatches[partialMatches.length - 1].route;3318 if (route.path === "*") {3319 // The splat is still our most accurate partial, so run with it3320 return { type: "success", matches: partialMatches };3321 }3322 3322 } 3323 3323 } … … 3493 3493 requestContext, 3494 3494 skipLoaderErrorBubbling, 3495 unstable_dataStrategy,3495 dataStrategy, 3496 3496 }: { 3497 3497 requestContext?: unknown; 3498 3498 skipLoaderErrorBubbling?: boolean; 3499 unstable_dataStrategy?: DataStrategyFunction;3499 dataStrategy?: DataStrategyFunction; 3500 3500 } = {} 3501 3501 ): Promise<StaticHandlerContext | Response> { … … 3549 3549 matches, 3550 3550 requestContext, 3551 unstable_dataStrategy || null,3551 dataStrategy || null, 3552 3552 skipLoaderErrorBubbling === true, 3553 3553 null … … 3594 3594 routeId, 3595 3595 requestContext, 3596 unstable_dataStrategy,3596 dataStrategy, 3597 3597 }: { 3598 3598 requestContext?: unknown; 3599 3599 routeId?: string; 3600 unstable_dataStrategy?: DataStrategyFunction;3600 dataStrategy?: DataStrategyFunction; 3601 3601 } = {} 3602 3602 ): Promise<any> { … … 3632 3632 matches, 3633 3633 requestContext, 3634 unstable_dataStrategy || null,3634 dataStrategy || null, 3635 3635 false, 3636 3636 match … … 3671 3671 matches: AgnosticDataRouteMatch[], 3672 3672 requestContext: unknown, 3673 unstable_dataStrategy: DataStrategyFunction | null,3673 dataStrategy: DataStrategyFunction | null, 3674 3674 skipLoaderErrorBubbling: boolean, 3675 3675 routeMatch: AgnosticDataRouteMatch | null … … 3687 3687 routeMatch || getTargetMatch(matches, location), 3688 3688 requestContext, 3689 unstable_dataStrategy,3689 dataStrategy, 3690 3690 skipLoaderErrorBubbling, 3691 3691 routeMatch != null … … 3698 3698 matches, 3699 3699 requestContext, 3700 unstable_dataStrategy,3700 dataStrategy, 3701 3701 skipLoaderErrorBubbling, 3702 3702 routeMatch … … 3711 3711 } catch (e) { 3712 3712 // If the user threw/returned a Response in callLoaderOrAction for a 3713 // `queryRoute` call, we throw the ` HandlerResult` to bail out early3713 // `queryRoute` call, we throw the `DataStrategyResult` to bail out early 3714 3714 // and then return or throw the raw Response here accordingly 3715 if (is HandlerResult(e) && isResponse(e.result)) {3715 if (isDataStrategyResult(e) && isResponse(e.result)) { 3716 3716 if (e.type === ResultType.error) { 3717 3717 throw e.result; … … 3733 3733 actionMatch: AgnosticDataRouteMatch, 3734 3734 requestContext: unknown, 3735 unstable_dataStrategy: DataStrategyFunction | null,3735 dataStrategy: DataStrategyFunction | null, 3736 3736 skipLoaderErrorBubbling: boolean, 3737 3737 isRouteRequest: boolean … … 3760 3760 isRouteRequest, 3761 3761 requestContext, 3762 unstable_dataStrategy3762 dataStrategy 3763 3763 ); 3764 result = results[ 0];3764 result = results[actionMatch.route.id]; 3765 3765 3766 3766 if (request.signal.aborted) { … … 3832 3832 matches, 3833 3833 requestContext, 3834 unstable_dataStrategy,3834 dataStrategy, 3835 3835 skipLoaderErrorBubbling, 3836 3836 null, … … 3857 3857 matches, 3858 3858 requestContext, 3859 unstable_dataStrategy,3859 dataStrategy, 3860 3860 skipLoaderErrorBubbling, 3861 3861 null … … 3879 3879 matches: AgnosticDataRouteMatch[], 3880 3880 requestContext: unknown, 3881 unstable_dataStrategy: DataStrategyFunction | null,3881 dataStrategy: DataStrategyFunction | null, 3882 3882 skipLoaderErrorBubbling: boolean, 3883 3883 routeMatch: AgnosticDataRouteMatch | null, … … 3942 3942 isRouteRequest, 3943 3943 requestContext, 3944 unstable_dataStrategy3944 dataStrategy 3945 3945 ); 3946 3946 … … 3953 3953 let context = processRouteLoaderData( 3954 3954 matches, 3955 matchesToLoad,3956 3955 results, 3957 3956 pendingActionResult, … … 3989 3988 isRouteRequest: boolean, 3990 3989 requestContext: unknown, 3991 unstable_dataStrategy: DataStrategyFunction | null3992 ): Promise< DataResult[]> {3990 dataStrategy: DataStrategyFunction | null 3991 ): Promise<Record<string, DataResult>> { 3993 3992 let results = await callDataStrategyImpl( 3994 unstable_dataStrategy || defaultDataStrategy,3993 dataStrategy || defaultDataStrategy, 3995 3994 type, 3995 null, 3996 3996 request, 3997 3997 matchesToLoad, 3998 3998 matches, 3999 null, 3999 4000 manifest, 4000 4001 mapRouteProperties, … … 4002 4003 ); 4003 4004 4004 return await Promise.all( 4005 results.map((result, i) => { 4006 if (isRedirectHandlerResult(result)) { 4005 let dataResults: Record<string, DataResult> = {}; 4006 await Promise.all( 4007 matches.map(async (match) => { 4008 if (!(match.route.id in results)) { 4009 return; 4010 } 4011 let result = results[match.route.id]; 4012 if (isRedirectDataStrategyResultResult(result)) { 4007 4013 let response = result.result as Response; 4008 4014 // Throw redirects and let the server handle them with an HTTP redirect … … 4010 4016 response, 4011 4017 request, 4012 match esToLoad[i].route.id,4018 match.route.id, 4013 4019 matches, 4014 4020 basename, … … 4022 4028 } 4023 4029 4024 return convertHandlerResultToDataResult(result); 4030 dataResults[match.route.id] = 4031 await convertDataStrategyResultToDataResult(result); 4025 4032 }) 4026 4033 ); 4034 return dataResults; 4027 4035 } 4028 4036 … … 4126 4134 } 4127 4135 4128 // Add an ?index param for matched index routes if we don't already have one 4129 if ( 4130 (to == null || to === "" || to === ".") && 4131 activeRouteMatch && 4132 activeRouteMatch.route.index && 4133 !hasNakedIndexQuery(path.search) 4134 ) { 4135 path.search = path.search 4136 ? path.search.replace(/^\?/, "?index&") 4137 : "?index"; 4136 // Account for `?index` params when routing to the current location 4137 if ((to == null || to === "" || to === ".") && activeRouteMatch) { 4138 let nakedIndex = hasNakedIndexQuery(path.search); 4139 if (activeRouteMatch.route.index && !nakedIndex) { 4140 // Add one when we're targeting an index route 4141 path.search = path.search 4142 ? path.search.replace(/^\?/, "?index&") 4143 : "?index"; 4144 } else if (!activeRouteMatch.route.index && nakedIndex) { 4145 // Remove existing ones when we're not 4146 let params = new URLSearchParams(path.search); 4147 let indexValues = params.getAll("index"); 4148 params.delete("index"); 4149 indexValues.filter((v) => v).forEach((v) => params.append("index", v)); 4150 let qs = params.toString(); 4151 path.search = qs ? `?${qs}` : ""; 4152 } 4138 4153 } 4139 4154 … … 4299 4314 } 4300 4315 4301 // Filter out all routes below any caught error as they aren't going to4316 // Filter out all routes at/below any caught error as they aren't going to 4302 4317 // render so we don't need to load them 4303 4318 function getLoaderMatchesUntilBoundary( 4304 4319 matches: AgnosticDataRouteMatch[], 4305 boundaryId: string 4320 boundaryId: string, 4321 includeBoundary = false 4306 4322 ) { 4307 let boundaryMatches = matches; 4308 if (boundaryId) { 4309 let index = matches.findIndex((m) => m.route.id === boundaryId); 4310 if (index >= 0) { 4311 boundaryMatches = matches.slice(0, index); 4312 } 4313 } 4314 return boundaryMatches; 4323 let index = matches.findIndex((m) => m.route.id === boundaryId); 4324 if (index >= 0) { 4325 return matches.slice(0, includeBoundary ? index + 1 : index); 4326 } 4327 return matches; 4315 4328 } 4316 4329 … … 4321 4334 submission: Submission | undefined, 4322 4335 location: Location, 4323 i sInitialLoad: boolean,4336 initialHydration: boolean, 4324 4337 skipActionErrorRevalidation: boolean, 4325 4338 isRevalidationRequired: boolean, … … 4342 4355 4343 4356 // Pick navigation matches that are net-new or qualify for revalidation 4344 let boundaryId = 4345 pendingActionResult && isErrorResult(pendingActionResult[1]) 4346 ? pendingActionResult[0] 4347 : undefined; 4348 let boundaryMatches = boundaryId 4349 ? getLoaderMatchesUntilBoundary(matches, boundaryId) 4350 : matches; 4357 let boundaryMatches = matches; 4358 if (initialHydration && state.errors) { 4359 // On initial hydration, only consider matches up to _and including_ the boundary. 4360 // This is inclusive to handle cases where a server loader ran successfully, 4361 // a child server loader bubbled up to this route, but this route has 4362 // `clientLoader.hydrate` so we want to still run the `clientLoader` so that 4363 // we have a complete version of `loaderData` 4364 boundaryMatches = getLoaderMatchesUntilBoundary( 4365 matches, 4366 Object.keys(state.errors)[0], 4367 true 4368 ); 4369 } else if (pendingActionResult && isErrorResult(pendingActionResult[1])) { 4370 // If an action threw an error, we call loaders up to, but not including the 4371 // boundary 4372 boundaryMatches = getLoaderMatchesUntilBoundary( 4373 matches, 4374 pendingActionResult[0] 4375 ); 4376 } 4351 4377 4352 4378 // Don't revalidate loaders by default after action 4xx/5xx responses … … 4370 4396 } 4371 4397 4372 if (isInitialLoad) { 4373 if (typeof route.loader !== "function" || route.loader.hydrate) { 4374 return true; 4375 } 4376 return ( 4377 state.loaderData[route.id] === undefined && 4378 // Don't re-run if the loader ran and threw an error 4379 (!state.errors || state.errors[route.id] === undefined) 4380 ); 4398 if (initialHydration) { 4399 return shouldLoadRouteOnHydration(route, state.loaderData, state.errors); 4381 4400 } 4382 4401 … … 4420 4439 fetchLoadMatches.forEach((f, key) => { 4421 4440 // Don't revalidate: 4422 // - on initial load(shouldn't be any fetchers then anyway)4441 // - on initial hydration (shouldn't be any fetchers then anyway) 4423 4442 // - if fetcher won't be present in the subsequent render 4424 4443 // - no longer matches the URL (v7_fetcherPersist=false) 4425 4444 // - was unmounted but persisted due to v7_fetcherPersist=true 4426 4445 if ( 4427 i sInitialLoad||4446 initialHydration || 4428 4447 !matches.some((m) => m.route.id === f.routeId) || 4429 4448 deletedFetchers.has(key) … … 4505 4524 } 4506 4525 4526 function shouldLoadRouteOnHydration( 4527 route: AgnosticDataRouteObject, 4528 loaderData: RouteData | null | undefined, 4529 errors: RouteData | null | undefined 4530 ) { 4531 // We dunno if we have a loader - gotta find out! 4532 if (route.lazy) { 4533 return true; 4534 } 4535 4536 // No loader, nothing to initialize 4537 if (!route.loader) { 4538 return false; 4539 } 4540 4541 let hasData = loaderData != null && loaderData[route.id] !== undefined; 4542 let hasError = errors != null && errors[route.id] !== undefined; 4543 4544 // Don't run if we error'd during SSR 4545 if (!hasData && hasError) { 4546 return false; 4547 } 4548 4549 // Explicitly opting-in to running on hydration 4550 if (typeof route.loader === "function" && route.loader.hydrate === true) { 4551 return true; 4552 } 4553 4554 // Otherwise, run if we're not yet initialized with anything 4555 return !hasData && !hasError; 4556 } 4557 4507 4558 function isNewLoader( 4508 4559 currentLoaderData: RouteData, … … 4554 4605 } 4555 4606 4556 /**4557 * Idempotent utility to execute patchRoutesOnMiss() to lazily load route4558 * definitions and update the routes/routeManifest4559 */4560 async function loadLazyRouteChildren(4561 patchRoutesOnMissImpl: AgnosticPatchRoutesOnMissFunction,4562 path: string,4563 matches: AgnosticDataRouteMatch[],4564 routes: AgnosticDataRouteObject[],4565 manifest: RouteManifest,4566 mapRouteProperties: MapRoutePropertiesFunction,4567 pendingRouteChildren: Map<string, ReturnType<typeof patchRoutesOnMissImpl>>,4568 signal: AbortSignal4569 ) {4570 let key = [path, ...matches.map((m) => m.route.id)].join("-");4571 try {4572 let pending = pendingRouteChildren.get(key);4573 if (!pending) {4574 pending = patchRoutesOnMissImpl({4575 path,4576 matches,4577 patch: (routeId, children) => {4578 if (!signal.aborted) {4579 patchRoutesImpl(4580 routeId,4581 children,4582 routes,4583 manifest,4584 mapRouteProperties4585 );4586 }4587 },4588 });4589 pendingRouteChildren.set(key, pending);4590 }4591 4592 if (pending && isPromise<AgnosticRouteObject[]>(pending)) {4593 await pending;4594 }4595 } finally {4596 pendingRouteChildren.delete(key);4597 }4598 }4599 4600 4607 function patchRoutesImpl( 4601 4608 routeId: string | null, … … 4605 4612 mapRouteProperties: MapRoutePropertiesFunction 4606 4613 ) { 4614 let childrenToPatch: AgnosticDataRouteObject[]; 4607 4615 if (routeId) { 4608 4616 let route = manifest[routeId]; … … 4611 4619 `No route found to patch children into: routeId = ${routeId}` 4612 4620 ); 4613 let dataChildren = convertRoutesToDataRoutes( 4614 children, 4615 mapRouteProperties, 4616 [routeId, "patch", String(route.children?.length || "0")], 4617 manifest 4618 ); 4619 if (route.children) { 4620 route.children.push(...dataChildren); 4621 } else { 4622 route.children = dataChildren; 4623 } 4621 if (!route.children) { 4622 route.children = []; 4623 } 4624 childrenToPatch = route.children; 4624 4625 } else { 4625 let dataChildren = convertRoutesToDataRoutes( 4626 children, 4627 mapRouteProperties, 4628 ["patch", String(routesToUse.length || "0")], 4629 manifest 4630 ); 4631 routesToUse.push(...dataChildren); 4632 } 4626 childrenToPatch = routesToUse; 4627 } 4628 4629 // Don't patch in routes we already know about so that `patch` is idempotent 4630 // to simplify user-land code. This is useful because we re-call the 4631 // `patchRoutesOnNavigation` function for matched routes with params. 4632 let uniqueChildren = children.filter( 4633 (newRoute) => 4634 !childrenToPatch.some((existingRoute) => 4635 isSameRoute(newRoute, existingRoute) 4636 ) 4637 ); 4638 4639 let newRoutes = convertRoutesToDataRoutes( 4640 uniqueChildren, 4641 mapRouteProperties, 4642 [routeId || "_", "patch", String(childrenToPatch?.length || "0")], 4643 manifest 4644 ); 4645 4646 childrenToPatch.push(...newRoutes); 4647 } 4648 4649 function isSameRoute( 4650 newRoute: AgnosticRouteObject, 4651 existingRoute: AgnosticRouteObject 4652 ): boolean { 4653 // Most optimal check is by id 4654 if ( 4655 "id" in newRoute && 4656 "id" in existingRoute && 4657 newRoute.id === existingRoute.id 4658 ) { 4659 return true; 4660 } 4661 4662 // Second is by pathing differences 4663 if ( 4664 !( 4665 newRoute.index === existingRoute.index && 4666 newRoute.path === existingRoute.path && 4667 newRoute.caseSensitive === existingRoute.caseSensitive 4668 ) 4669 ) { 4670 return false; 4671 } 4672 4673 // Pathless layout routes are trickier since we need to check children. 4674 // If they have no children then they're the same as far as we can tell 4675 if ( 4676 (!newRoute.children || newRoute.children.length === 0) && 4677 (!existingRoute.children || existingRoute.children.length === 0) 4678 ) { 4679 return true; 4680 } 4681 4682 // Otherwise, we look to see if every child in the new route is already 4683 // represented in the existing route's children 4684 return newRoute.children!.every((aChild, i) => 4685 existingRoute.children?.some((bChild) => isSameRoute(aChild, bChild)) 4686 ); 4633 4687 } 4634 4688 … … 4712 4766 4713 4767 // Default implementation of `dataStrategy` which fetches all loaders in parallel 4714 function defaultDataStrategy( 4715 opts: DataStrategyFunctionArgs 4716 ): ReturnType<DataStrategyFunction> { 4717 return Promise.all(opts.matches.map((m) => m.resolve())); 4768 async function defaultDataStrategy({ 4769 matches, 4770 }: DataStrategyFunctionArgs): ReturnType<DataStrategyFunction> { 4771 let matchesToLoad = matches.filter((m) => m.shouldLoad); 4772 let results = await Promise.all(matchesToLoad.map((m) => m.resolve())); 4773 return results.reduce( 4774 (acc, result, i) => 4775 Object.assign(acc, { [matchesToLoad[i].route.id]: result }), 4776 {} 4777 ); 4718 4778 } 4719 4779 … … 4721 4781 dataStrategyImpl: DataStrategyFunction, 4722 4782 type: "loader" | "action", 4783 state: RouterState | null, 4723 4784 request: Request, 4724 4785 matchesToLoad: AgnosticDataRouteMatch[], 4725 4786 matches: AgnosticDataRouteMatch[], 4787 fetcherKey: string | null, 4726 4788 manifest: RouteManifest, 4727 4789 mapRouteProperties: MapRoutePropertiesFunction, 4728 4790 requestContext?: unknown 4729 ): Promise<HandlerResult[]> { 4730 let routeIdsToLoad = matchesToLoad.reduce( 4731 (acc, m) => acc.add(m.route.id), 4732 new Set<string>() 4791 ): Promise<Record<string, DataStrategyResult>> { 4792 let loadRouteDefinitionsPromises = matches.map((m) => 4793 m.route.lazy 4794 ? loadLazyRouteModule(m.route, mapRouteProperties, manifest) 4795 : undefined 4733 4796 ); 4734 let loadedMatches = new Set<string>(); 4797 4798 let dsMatches = matches.map((match, i) => { 4799 let loadRoutePromise = loadRouteDefinitionsPromises[i]; 4800 let shouldLoad = matchesToLoad.some((m) => m.route.id === match.route.id); 4801 // `resolve` encapsulates route.lazy(), executing the loader/action, 4802 // and mapping return values/thrown errors to a `DataStrategyResult`. Users 4803 // can pass a callback to take fine-grained control over the execution 4804 // of the loader/action 4805 let resolve: DataStrategyMatch["resolve"] = async (handlerOverride) => { 4806 if ( 4807 handlerOverride && 4808 request.method === "GET" && 4809 (match.route.lazy || match.route.loader) 4810 ) { 4811 shouldLoad = true; 4812 } 4813 return shouldLoad 4814 ? callLoaderOrAction( 4815 type, 4816 request, 4817 match, 4818 loadRoutePromise, 4819 handlerOverride, 4820 requestContext 4821 ) 4822 : Promise.resolve({ type: ResultType.data, result: undefined }); 4823 }; 4824 4825 return { 4826 ...match, 4827 shouldLoad, 4828 resolve, 4829 }; 4830 }); 4735 4831 4736 4832 // Send all matches here to allow for a middleware-type implementation. … … 4738 4834 // back out below. 4739 4835 let results = await dataStrategyImpl({ 4740 matches: matches.map((match) => { 4741 let shouldLoad = routeIdsToLoad.has(match.route.id); 4742 // `resolve` encapsulates the route.lazy, executing the 4743 // loader/action, and mapping return values/thrown errors to a 4744 // HandlerResult. Users can pass a callback to take fine-grained control 4745 // over the execution of the loader/action 4746 let resolve: DataStrategyMatch["resolve"] = (handlerOverride) => { 4747 loadedMatches.add(match.route.id); 4748 return shouldLoad 4749 ? callLoaderOrAction( 4750 type, 4751 request, 4752 match, 4753 manifest, 4754 mapRouteProperties, 4755 handlerOverride, 4756 requestContext 4757 ) 4758 : Promise.resolve({ type: ResultType.data, result: undefined }); 4759 }; 4760 4761 return { 4762 ...match, 4763 shouldLoad, 4764 resolve, 4765 }; 4766 }), 4836 matches: dsMatches, 4767 4837 request, 4768 4838 params: matches[0].params, 4839 fetcherKey, 4769 4840 context: requestContext, 4770 4841 }); 4771 4842 4772 // Throw if any loadRoute implementations not called since they are what 4773 // ensures a route is fully loaded 4774 matches.forEach((m) => 4775 invariant( 4776 loadedMatches.has(m.route.id), 4777 `\`match.resolve()\` was not called for route id "${m.route.id}". ` + 4778 "You must call `match.resolve()` on every match passed to " + 4779 "`dataStrategy` to ensure all routes are properly loaded." 4780 ) 4781 ); 4782 4783 // Filter out any middleware-only matches for which we didn't need to run handlers 4784 return results.filter((_, i) => routeIdsToLoad.has(matches[i].route.id)); 4843 // Wait for all routes to load here but 'swallow the error since we want 4844 // it to bubble up from the `await loadRoutePromise` in `callLoaderOrAction` - 4845 // called from `match.resolve()` 4846 try { 4847 await Promise.all(loadRouteDefinitionsPromises); 4848 } catch (e) { 4849 // No-op 4850 } 4851 4852 return results; 4785 4853 } 4786 4854 … … 4790 4858 request: Request, 4791 4859 match: AgnosticDataRouteMatch, 4792 manifest: RouteManifest, 4793 mapRouteProperties: MapRoutePropertiesFunction, 4860 loadRoutePromise: Promise<void> | undefined, 4794 4861 handlerOverride: Parameters<DataStrategyMatch["resolve"]>[0], 4795 4862 staticContext?: unknown 4796 ): Promise< HandlerResult> {4797 let result: HandlerResult;4863 ): Promise<DataStrategyResult> { 4864 let result: DataStrategyResult; 4798 4865 let onReject: (() => void) | undefined; 4799 4866 4800 4867 let runHandler = ( 4801 4868 handler: AgnosticRouteObject["loader"] | AgnosticRouteObject["action"] 4802 ): Promise< HandlerResult> => {4869 ): Promise<DataStrategyResult> => { 4803 4870 // Setup a promise we can race against so that abort signals short circuit 4804 4871 let reject: () => void; 4805 // This will never resolve so safe to type it as Promise< HandlerResult> to4872 // This will never resolve so safe to type it as Promise<DataStrategyResult> to 4806 4873 // satisfy the function return value 4807 let abortPromise = new Promise< HandlerResult>((_, r) => (reject = r));4874 let abortPromise = new Promise<DataStrategyResult>((_, r) => (reject = r)); 4808 4875 onReject = () => reject(); 4809 4876 request.signal.addEventListener("abort", onReject); … … 4828 4895 }; 4829 4896 4830 let handlerPromise: Promise<HandlerResult>; 4831 if (handlerOverride) { 4832 handlerPromise = handlerOverride((ctx: unknown) => actualHandler(ctx)); 4833 } else { 4834 handlerPromise = (async () => { 4835 try { 4836 let val = await actualHandler(); 4837 return { type: "data", result: val }; 4838 } catch (e) { 4839 return { type: "error", result: e }; 4840 } 4841 })(); 4842 } 4897 let handlerPromise: Promise<DataStrategyResult> = (async () => { 4898 try { 4899 let val = await (handlerOverride 4900 ? handlerOverride((ctx: unknown) => actualHandler(ctx)) 4901 : actualHandler()); 4902 return { type: "data", result: val }; 4903 } catch (e) { 4904 return { type: "error", result: e }; 4905 } 4906 })(); 4843 4907 4844 4908 return Promise.race([handlerPromise, abortPromise]); … … 4848 4912 let handler = match.route[type]; 4849 4913 4850 if (match.route.lazy) { 4914 // If we have a route.lazy promise, await that first 4915 if (loadRoutePromise) { 4851 4916 if (handler) { 4852 4917 // Run statically defined handler in parallel with lazy() … … 4859 4924 handlerError = e; 4860 4925 }), 4861 load LazyRouteModule(match.route, mapRouteProperties, manifest),4926 loadRoutePromise, 4862 4927 ]); 4863 4928 if (handlerError !== undefined) { … … 4867 4932 } else { 4868 4933 // Load lazy route module, then run any returned handler 4869 await load LazyRouteModule(match.route, mapRouteProperties, manifest);4934 await loadRoutePromise; 4870 4935 4871 4936 handler = match.route[type]; … … 4907 4972 } catch (e) { 4908 4973 // We should already be catching and converting normal handler executions to 4909 // HandlerResults and returning them, so anything that throws here is an4974 // DataStrategyResults and returning them, so anything that throws here is an 4910 4975 // unexpected error we still need to wrap 4911 4976 return { type: ResultType.error, result: e }; … … 4919 4984 } 4920 4985 4921 async function convert HandlerResultToDataResult(4922 handlerResult: HandlerResult4986 async function convertDataStrategyResultToDataResult( 4987 dataStrategyResult: DataStrategyResult 4923 4988 ): Promise<DataResult> { 4924 let { result, type } = handlerResult;4989 let { result, type } = dataStrategyResult; 4925 4990 4926 4991 if (isResponse(result)) { … … 4971 5036 } 4972 5037 4973 // Convert thrown unstable_data() to ErrorResponse instances5038 // Convert thrown data() to ErrorResponse instances 4974 5039 result = new ErrorResponseImpl( 4975 5040 result.init?.status || 500, … … 5124 5189 function processRouteLoaderData( 5125 5190 matches: AgnosticDataRouteMatch[], 5126 matchesToLoad: AgnosticDataRouteMatch[], 5127 results: DataResult[], 5191 results: Record<string, DataResult>, 5128 5192 pendingActionResult: PendingActionResult | undefined, 5129 5193 activeDeferreds: Map<string, DeferredData>, … … 5147 5211 5148 5212 // Process loader results into state.loaderData/state.errors 5149 results.forEach((result, index) => { 5150 let id = matchesToLoad[index].route.id; 5213 matches.forEach((match) => { 5214 if (!(match.route.id in results)) { 5215 return; 5216 } 5217 let id = match.route.id; 5218 let result = results[id]; 5151 5219 invariant( 5152 5220 !isRedirectResult(result), … … 5240 5308 state: RouterState, 5241 5309 matches: AgnosticDataRouteMatch[], 5242 matchesToLoad: AgnosticDataRouteMatch[], 5243 results: DataResult[], 5310 results: Record<string, DataResult>, 5244 5311 pendingActionResult: PendingActionResult | undefined, 5245 5312 revalidatingFetchers: RevalidatingFetcher[], 5246 fetcherResults: DataResult[],5313 fetcherResults: Record<string, DataResult>, 5247 5314 activeDeferreds: Map<string, DeferredData> 5248 5315 ): { … … 5252 5319 let { loaderData, errors } = processRouteLoaderData( 5253 5320 matches, 5254 matchesToLoad,5255 5321 results, 5256 5322 pendingActionResult, … … 5260 5326 5261 5327 // Process results from our revalidating fetchers 5262 for (let index = 0; index < revalidatingFetchers.length; index++) { 5263 let { key, match, controller } = revalidatingFetchers[index]; 5264 invariant( 5265 fetcherResults !== undefined && fetcherResults[index] !== undefined, 5266 "Did not find corresponding fetcher result" 5267 ); 5268 let result = fetcherResults[index]; 5328 revalidatingFetchers.forEach((rf) => { 5329 let { key, match, controller } = rf; 5330 let result = fetcherResults[key]; 5331 invariant(result, "Did not find corresponding fetcher result"); 5269 5332 5270 5333 // Process fetcher non-redirect errors 5271 5334 if (controller && controller.signal.aborted) { 5272 5335 // Nothing to do for aborted fetchers 5273 continue;5336 return; 5274 5337 } else if (isErrorResult(result)) { 5275 5338 let boundaryMatch = findNearestBoundary(state.matches, match?.route.id); … … 5293 5356 state.fetchers.set(key, doneFetcher); 5294 5357 } 5295 } 5358 }); 5296 5359 5297 5360 return { loaderData, errors }; … … 5400 5463 routeId?: string; 5401 5464 method?: string; 5402 type?: "defer-action" | "invalid-body" | "route-discovery";5465 type?: "defer-action" | "invalid-body"; 5403 5466 message?: string; 5404 5467 } = {} … … 5409 5472 if (status === 400) { 5410 5473 statusText = "Bad Request"; 5411 if (type === "route-discovery") { 5412 errorMessage = 5413 `Unable to match URL "${pathname}" - the \`unstable_patchRoutesOnMiss()\` ` + 5414 `function threw the following error:\n${message}`; 5415 } else if (method && pathname && routeId) { 5474 if (method && pathname && routeId) { 5416 5475 errorMessage = 5417 5476 `You made a ${method} request to "${pathname}" but ` + … … 5451 5510 // Find any returned redirect errors, starting from the lowest match 5452 5511 function findRedirect( 5453 results: DataResult[] 5454 ): { result: RedirectResult; idx: number } | undefined { 5455 for (let i = results.length - 1; i >= 0; i--) { 5456 let result = results[i]; 5512 results: Record<string, DataResult> 5513 ): { key: string; result: RedirectResult } | undefined { 5514 let entries = Object.entries(results); 5515 for (let i = entries.length - 1; i >= 0; i--) { 5516 let [key, result] = entries[i]; 5457 5517 if (isRedirectResult(result)) { 5458 return { result, idx: i};5518 return { key, result }; 5459 5519 } 5460 5520 } … … 5491 5551 } 5492 5552 5493 function is HandlerResult(result: unknown): result is HandlerResult {5553 function isDataStrategyResult(result: unknown): result is DataStrategyResult { 5494 5554 return ( 5495 5555 result != null && … … 5501 5561 } 5502 5562 5503 function isRedirect HandlerResult(result: HandlerResult) {5563 function isRedirectDataStrategyResultResult(result: DataStrategyResult) { 5504 5564 return ( 5505 5565 isResponse(result.result) && redirectStatusCodes.has(result.result.status) … … 5574 5634 } 5575 5635 5576 async function resolveDeferredResults( 5636 async function resolveNavigationDeferredResults( 5637 matches: (AgnosticDataRouteMatch | null)[], 5638 results: Record<string, DataResult>, 5639 signal: AbortSignal, 5577 5640 currentMatches: AgnosticDataRouteMatch[], 5578 matchesToLoad: (AgnosticDataRouteMatch | null)[], 5579 results: DataResult[], 5580 signals: (AbortSignal | null)[], 5581 isFetcher: boolean, 5582 currentLoaderData?: RouteData 5641 currentLoaderData: RouteData 5583 5642 ) { 5584 for (let index = 0; index < results.length; index++) { 5585 let result = results[index]; 5586 let match = matchesToLoad[index]; 5643 let entries = Object.entries(results); 5644 for (let index = 0; index < entries.length; index++) { 5645 let [routeId, result] = entries[index]; 5646 let match = matches.find((m) => m?.route.id === routeId); 5587 5647 // If we don't have a match, then we can have a deferred result to do 5588 5648 // anything with. This is for revalidating fetchers where the route was … … 5600 5660 (currentLoaderData && currentLoaderData[match.route.id]) !== undefined; 5601 5661 5602 if (isDeferredResult(result) && (isFetcher || isRevalidatingLoader)) {5662 if (isDeferredResult(result) && isRevalidatingLoader) { 5603 5663 // Note: we do not have to touch activeDeferreds here since we race them 5604 5664 // against the signal in resolveDeferredData and they'll get aborted 5605 5665 // there if needed 5606 let signal = signals[index]; 5607 invariant( 5608 signal, 5609 "Expected an AbortSignal for revalidating fetcher deferred result" 5610 ); 5611 await resolveDeferredData(result, signal, isFetcher).then((result) => { 5666 await resolveDeferredData(result, signal, false).then((result) => { 5612 5667 if (result) { 5613 results[ index] = result || results[index];5668 results[routeId] = result; 5614 5669 } 5615 5670 }); 5671 } 5672 } 5673 } 5674 5675 async function resolveFetcherDeferredResults( 5676 matches: (AgnosticDataRouteMatch | null)[], 5677 results: Record<string, DataResult>, 5678 revalidatingFetchers: RevalidatingFetcher[] 5679 ) { 5680 for (let index = 0; index < revalidatingFetchers.length; index++) { 5681 let { key, routeId, controller } = revalidatingFetchers[index]; 5682 let result = results[key]; 5683 let match = matches.find((m) => m?.route.id === routeId); 5684 // If we don't have a match, then we can have a deferred result to do 5685 // anything with. This is for revalidating fetchers where the route was 5686 // removed during HMR 5687 if (!match) { 5688 continue; 5689 } 5690 5691 if (isDeferredResult(result)) { 5692 // Note: we do not have to touch activeDeferreds here since we race them 5693 // against the signal in resolveDeferredData and they'll get aborted 5694 // there if needed 5695 invariant( 5696 controller, 5697 "Expected an AbortController for revalidating fetcher deferred result" 5698 ); 5699 await resolveDeferredData(result, controller.signal, true).then( 5700 (result) => { 5701 if (result) { 5702 results[key] = result; 5703 } 5704 } 5705 ); 5616 5706 } 5617 5707 }
Note:
See TracChangeset
for help on using the changeset viewer.