@remix-run/router 1.15.3 → 1.16.0-pre.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/dist/index.d.ts +1 -1
- package/dist/router.cjs.js +441 -250
- package/dist/router.cjs.js.map +1 -1
- package/dist/router.d.ts +7 -1
- package/dist/router.js +427 -238
- package/dist/router.js.map +1 -1
- package/dist/router.umd.js +441 -250
- package/dist/router.umd.js.map +1 -1
- package/dist/router.umd.min.js +2 -2
- package/dist/router.umd.min.js.map +1 -1
- package/dist/utils.d.ts +28 -10
- package/index.ts +4 -0
- package/package.json +2 -2
- package/router.ts +747 -359
- package/utils.ts +46 -14
package/router.ts
CHANGED
|
@@ -8,11 +8,13 @@ import {
|
|
|
8
8
|
warning,
|
|
9
9
|
} from "./history";
|
|
10
10
|
import type {
|
|
11
|
-
ActionFunction,
|
|
12
11
|
AgnosticDataRouteMatch,
|
|
13
12
|
AgnosticDataRouteObject,
|
|
13
|
+
DataStrategyMatch,
|
|
14
14
|
AgnosticRouteObject,
|
|
15
15
|
DataResult,
|
|
16
|
+
DataStrategyFunction,
|
|
17
|
+
DataStrategyFunctionArgs,
|
|
16
18
|
DeferredData,
|
|
17
19
|
DeferredResult,
|
|
18
20
|
DetectErrorBoundaryFunction,
|
|
@@ -20,8 +22,8 @@ import type {
|
|
|
20
22
|
FormEncType,
|
|
21
23
|
FormMethod,
|
|
22
24
|
HTMLFormMethod,
|
|
25
|
+
HandlerResult,
|
|
23
26
|
ImmutableRouteKey,
|
|
24
|
-
LoaderFunction,
|
|
25
27
|
MapRoutePropertiesFunction,
|
|
26
28
|
MutationFormMethod,
|
|
27
29
|
RedirectResult,
|
|
@@ -357,6 +359,7 @@ export interface FutureConfig {
|
|
|
357
359
|
v7_partialHydration: boolean;
|
|
358
360
|
v7_prependBasename: boolean;
|
|
359
361
|
v7_relativeSplatPath: boolean;
|
|
362
|
+
unstable_skipActionErrorRevalidation: boolean;
|
|
360
363
|
}
|
|
361
364
|
|
|
362
365
|
/**
|
|
@@ -374,6 +377,7 @@ export interface RouterInit {
|
|
|
374
377
|
future?: Partial<FutureConfig>;
|
|
375
378
|
hydrationData?: HydrationState;
|
|
376
379
|
window?: Window;
|
|
380
|
+
unstable_dataStrategy?: DataStrategyFunction;
|
|
377
381
|
}
|
|
378
382
|
|
|
379
383
|
/**
|
|
@@ -400,7 +404,13 @@ export interface StaticHandler {
|
|
|
400
404
|
dataRoutes: AgnosticDataRouteObject[];
|
|
401
405
|
query(
|
|
402
406
|
request: Request,
|
|
403
|
-
opts?: {
|
|
407
|
+
opts?: {
|
|
408
|
+
loadRouteIds?: string[];
|
|
409
|
+
requestContext?: unknown;
|
|
410
|
+
skipLoaders?: boolean;
|
|
411
|
+
skipLoaderErrorBubbling?: boolean;
|
|
412
|
+
unstable_dataStrategy?: DataStrategyFunction;
|
|
413
|
+
}
|
|
404
414
|
): Promise<StaticHandlerContext | Response>;
|
|
405
415
|
queryRoute(
|
|
406
416
|
request: Request,
|
|
@@ -616,18 +626,14 @@ interface ShortCircuitable {
|
|
|
616
626
|
shortCircuited?: boolean;
|
|
617
627
|
}
|
|
618
628
|
|
|
629
|
+
type PendingActionResult = [string, SuccessResult | ErrorResult];
|
|
630
|
+
|
|
619
631
|
interface HandleActionResult extends ShortCircuitable {
|
|
620
632
|
/**
|
|
621
|
-
*
|
|
622
|
-
*
|
|
623
|
-
* loaders have completed
|
|
624
|
-
*/
|
|
625
|
-
pendingActionError?: RouteData;
|
|
626
|
-
/**
|
|
627
|
-
* Data returned from the current action, keyed by the route owning the action.
|
|
628
|
-
* To be committed to the state after loaders have completed
|
|
633
|
+
* Tuple for the returned or thrown value from the current action. The routeId
|
|
634
|
+
* is the action route for success and the bubbled boundary route for errors.
|
|
629
635
|
*/
|
|
630
|
-
|
|
636
|
+
pendingActionResult?: PendingActionResult;
|
|
631
637
|
}
|
|
632
638
|
|
|
633
639
|
interface HandleLoadersResult extends ShortCircuitable {
|
|
@@ -660,16 +666,6 @@ interface RevalidatingFetcher extends FetchLoadMatch {
|
|
|
660
666
|
controller: AbortController | null;
|
|
661
667
|
}
|
|
662
668
|
|
|
663
|
-
/**
|
|
664
|
-
* Wrapper object to allow us to throw any response out from callLoaderOrAction
|
|
665
|
-
* for queryRouter while preserving whether or not it was thrown or returned
|
|
666
|
-
* from the loader/action
|
|
667
|
-
*/
|
|
668
|
-
interface QueryRouteResponse {
|
|
669
|
-
type: ResultType.data | ResultType.error;
|
|
670
|
-
response: Response;
|
|
671
|
-
}
|
|
672
|
-
|
|
673
669
|
const validMutationMethodsArr: MutationFormMethod[] = [
|
|
674
670
|
"post",
|
|
675
671
|
"put",
|
|
@@ -776,6 +772,7 @@ export function createRouter(init: RouterInit): Router {
|
|
|
776
772
|
);
|
|
777
773
|
let inFlightDataRoutes: AgnosticDataRouteObject[] | undefined;
|
|
778
774
|
let basename = init.basename || "/";
|
|
775
|
+
let dataStrategyImpl = init.unstable_dataStrategy || defaultDataStrategy;
|
|
779
776
|
// Config driven behavior flags
|
|
780
777
|
let future: FutureConfig = {
|
|
781
778
|
v7_fetcherPersist: false,
|
|
@@ -783,6 +780,7 @@ export function createRouter(init: RouterInit): Router {
|
|
|
783
780
|
v7_partialHydration: false,
|
|
784
781
|
v7_prependBasename: false,
|
|
785
782
|
v7_relativeSplatPath: false,
|
|
783
|
+
unstable_skipActionErrorRevalidation: false,
|
|
786
784
|
...init.future,
|
|
787
785
|
};
|
|
788
786
|
// Cleanup function for history
|
|
@@ -835,9 +833,16 @@ export function createRouter(init: RouterInit): Router {
|
|
|
835
833
|
let errors = init.hydrationData ? init.hydrationData.errors : null;
|
|
836
834
|
let isRouteInitialized = (m: AgnosticDataRouteMatch) => {
|
|
837
835
|
// No loader, nothing to initialize
|
|
838
|
-
if (!m.route.loader)
|
|
836
|
+
if (!m.route.loader) {
|
|
837
|
+
return true;
|
|
838
|
+
}
|
|
839
839
|
// Explicitly opting-in to running on hydration
|
|
840
|
-
if (
|
|
840
|
+
if (
|
|
841
|
+
typeof m.route.loader === "function" &&
|
|
842
|
+
m.route.loader.hydrate === true
|
|
843
|
+
) {
|
|
844
|
+
return false;
|
|
845
|
+
}
|
|
841
846
|
// Otherwise, initialized if hydrated with data or an error
|
|
842
847
|
return (
|
|
843
848
|
(loaderData && loaderData[m.route.id] !== undefined) ||
|
|
@@ -1493,24 +1498,24 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1493
1498
|
pendingNavigationController.signal,
|
|
1494
1499
|
opts && opts.submission
|
|
1495
1500
|
);
|
|
1496
|
-
let
|
|
1497
|
-
let pendingError: RouteData | undefined;
|
|
1501
|
+
let pendingActionResult: PendingActionResult | undefined;
|
|
1498
1502
|
|
|
1499
1503
|
if (opts && opts.pendingError) {
|
|
1500
1504
|
// If we have a pendingError, it means the user attempted a GET submission
|
|
1501
1505
|
// with binary FormData so assign here and skip to handleLoaders. That
|
|
1502
1506
|
// way we handle calling loaders above the boundary etc. It's not really
|
|
1503
1507
|
// different from an actionError in that sense.
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1508
|
+
pendingActionResult = [
|
|
1509
|
+
findNearestBoundary(matches).route.id,
|
|
1510
|
+
{ type: ResultType.error, error: opts.pendingError },
|
|
1511
|
+
];
|
|
1507
1512
|
} else if (
|
|
1508
1513
|
opts &&
|
|
1509
1514
|
opts.submission &&
|
|
1510
1515
|
isMutationMethod(opts.submission.formMethod)
|
|
1511
1516
|
) {
|
|
1512
1517
|
// Call action if we received an action submission
|
|
1513
|
-
let
|
|
1518
|
+
let actionResult = await handleAction(
|
|
1514
1519
|
request,
|
|
1515
1520
|
location,
|
|
1516
1521
|
opts.submission,
|
|
@@ -1518,17 +1523,20 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1518
1523
|
{ replace: opts.replace, flushSync }
|
|
1519
1524
|
);
|
|
1520
1525
|
|
|
1521
|
-
if (
|
|
1526
|
+
if (actionResult.shortCircuited) {
|
|
1522
1527
|
return;
|
|
1523
1528
|
}
|
|
1524
1529
|
|
|
1525
|
-
|
|
1526
|
-
pendingError = actionOutput.pendingActionError;
|
|
1530
|
+
pendingActionResult = actionResult.pendingActionResult;
|
|
1527
1531
|
loadingNavigation = getLoadingNavigation(location, opts.submission);
|
|
1528
1532
|
flushSync = false;
|
|
1529
1533
|
|
|
1530
1534
|
// Create a GET request for the loaders
|
|
1531
|
-
request =
|
|
1535
|
+
request = createClientSideRequest(
|
|
1536
|
+
init.history,
|
|
1537
|
+
request.url,
|
|
1538
|
+
request.signal
|
|
1539
|
+
);
|
|
1532
1540
|
}
|
|
1533
1541
|
|
|
1534
1542
|
// Call loaders
|
|
@@ -1542,8 +1550,7 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1542
1550
|
opts && opts.replace,
|
|
1543
1551
|
opts && opts.initialHydration === true,
|
|
1544
1552
|
flushSync,
|
|
1545
|
-
|
|
1546
|
-
pendingError
|
|
1553
|
+
pendingActionResult
|
|
1547
1554
|
);
|
|
1548
1555
|
|
|
1549
1556
|
if (shortCircuited) {
|
|
@@ -1557,7 +1564,7 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1557
1564
|
|
|
1558
1565
|
completeNavigation(location, {
|
|
1559
1566
|
matches,
|
|
1560
|
-
...(
|
|
1567
|
+
...getActionDataForCommit(pendingActionResult),
|
|
1561
1568
|
loaderData,
|
|
1562
1569
|
errors,
|
|
1563
1570
|
});
|
|
@@ -1592,16 +1599,13 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1592
1599
|
}),
|
|
1593
1600
|
};
|
|
1594
1601
|
} else {
|
|
1595
|
-
|
|
1602
|
+
let results = await callDataStrategy(
|
|
1596
1603
|
"action",
|
|
1597
1604
|
request,
|
|
1598
|
-
actionMatch,
|
|
1599
|
-
matches
|
|
1600
|
-
manifest,
|
|
1601
|
-
mapRouteProperties,
|
|
1602
|
-
basename,
|
|
1603
|
-
future.v7_relativeSplatPath
|
|
1605
|
+
[actionMatch],
|
|
1606
|
+
matches
|
|
1604
1607
|
);
|
|
1608
|
+
result = results[0];
|
|
1605
1609
|
|
|
1606
1610
|
if (request.signal.aborted) {
|
|
1607
1611
|
return { shortCircuited: true };
|
|
@@ -1616,13 +1620,24 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1616
1620
|
// If the user didn't explicity indicate replace behavior, replace if
|
|
1617
1621
|
// we redirected to the exact same location we're currently at to avoid
|
|
1618
1622
|
// double back-buttons
|
|
1619
|
-
|
|
1620
|
-
result.
|
|
1623
|
+
let location = normalizeRedirectLocation(
|
|
1624
|
+
result.response.headers.get("Location")!,
|
|
1625
|
+
new URL(request.url),
|
|
1626
|
+
basename
|
|
1627
|
+
);
|
|
1628
|
+
replace = location === state.location.pathname + state.location.search;
|
|
1621
1629
|
}
|
|
1622
|
-
await startRedirectNavigation(
|
|
1630
|
+
await startRedirectNavigation(request, result, {
|
|
1631
|
+
submission,
|
|
1632
|
+
replace,
|
|
1633
|
+
});
|
|
1623
1634
|
return { shortCircuited: true };
|
|
1624
1635
|
}
|
|
1625
1636
|
|
|
1637
|
+
if (isDeferredResult(result)) {
|
|
1638
|
+
throw getInternalRouterError(400, { type: "defer-action" });
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1626
1641
|
if (isErrorResult(result)) {
|
|
1627
1642
|
// Store off the pending error - we use it to determine which loaders
|
|
1628
1643
|
// to call and will commit it when we complete the navigation
|
|
@@ -1637,18 +1652,12 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1637
1652
|
}
|
|
1638
1653
|
|
|
1639
1654
|
return {
|
|
1640
|
-
|
|
1641
|
-
pendingActionData: {},
|
|
1642
|
-
pendingActionError: { [boundaryMatch.route.id]: result.error },
|
|
1655
|
+
pendingActionResult: [boundaryMatch.route.id, result],
|
|
1643
1656
|
};
|
|
1644
1657
|
}
|
|
1645
1658
|
|
|
1646
|
-
if (isDeferredResult(result)) {
|
|
1647
|
-
throw getInternalRouterError(400, { type: "defer-action" });
|
|
1648
|
-
}
|
|
1649
|
-
|
|
1650
1659
|
return {
|
|
1651
|
-
|
|
1660
|
+
pendingActionResult: [actionMatch.route.id, result],
|
|
1652
1661
|
};
|
|
1653
1662
|
}
|
|
1654
1663
|
|
|
@@ -1664,8 +1673,7 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1664
1673
|
replace?: boolean,
|
|
1665
1674
|
initialHydration?: boolean,
|
|
1666
1675
|
flushSync?: boolean,
|
|
1667
|
-
|
|
1668
|
-
pendingError?: RouteData
|
|
1676
|
+
pendingActionResult?: PendingActionResult
|
|
1669
1677
|
): Promise<HandleLoadersResult> {
|
|
1670
1678
|
// Figure out the right navigation we want to use for data loading
|
|
1671
1679
|
let loadingNavigation =
|
|
@@ -1686,6 +1694,7 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1686
1694
|
activeSubmission,
|
|
1687
1695
|
location,
|
|
1688
1696
|
future.v7_partialHydration && initialHydration === true,
|
|
1697
|
+
future.unstable_skipActionErrorRevalidation,
|
|
1689
1698
|
isRevalidationRequired,
|
|
1690
1699
|
cancelledDeferredRoutes,
|
|
1691
1700
|
cancelledFetcherLoads,
|
|
@@ -1694,8 +1703,7 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1694
1703
|
fetchRedirectIds,
|
|
1695
1704
|
routesToUse,
|
|
1696
1705
|
basename,
|
|
1697
|
-
|
|
1698
|
-
pendingError
|
|
1706
|
+
pendingActionResult
|
|
1699
1707
|
);
|
|
1700
1708
|
|
|
1701
1709
|
// Cancel pending deferreds for no-longer-matched routes or routes we're
|
|
@@ -1718,8 +1726,11 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1718
1726
|
matches,
|
|
1719
1727
|
loaderData: {},
|
|
1720
1728
|
// Commit pending error if we're short circuiting
|
|
1721
|
-
errors:
|
|
1722
|
-
|
|
1729
|
+
errors:
|
|
1730
|
+
pendingActionResult && isErrorResult(pendingActionResult[1])
|
|
1731
|
+
? { [pendingActionResult[0]]: pendingActionResult[1].error }
|
|
1732
|
+
: null,
|
|
1733
|
+
...getActionDataForCommit(pendingActionResult),
|
|
1723
1734
|
...(updatedFetchers ? { fetchers: new Map(state.fetchers) } : {}),
|
|
1724
1735
|
},
|
|
1725
1736
|
{ flushSync }
|
|
@@ -1745,15 +1756,27 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1745
1756
|
);
|
|
1746
1757
|
state.fetchers.set(rf.key, revalidatingFetcher);
|
|
1747
1758
|
});
|
|
1748
|
-
|
|
1759
|
+
|
|
1760
|
+
let actionData: Record<string, RouteData> | null | undefined;
|
|
1761
|
+
if (pendingActionResult && !isErrorResult(pendingActionResult[1])) {
|
|
1762
|
+
// This is cast to `any` currently because `RouteData`uses any and it
|
|
1763
|
+
// would be a breaking change to use any.
|
|
1764
|
+
// TODO: v7 - change `RouteData` to use `unknown` instead of `any`
|
|
1765
|
+
actionData = {
|
|
1766
|
+
[pendingActionResult[0]]: pendingActionResult[1].data as any,
|
|
1767
|
+
};
|
|
1768
|
+
} else if (state.actionData) {
|
|
1769
|
+
if (Object.keys(state.actionData).length === 0) {
|
|
1770
|
+
actionData = null;
|
|
1771
|
+
} else {
|
|
1772
|
+
actionData = state.actionData;
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1749
1776
|
updateState(
|
|
1750
1777
|
{
|
|
1751
1778
|
navigation: loadingNavigation,
|
|
1752
|
-
...(actionData
|
|
1753
|
-
? Object.keys(actionData).length === 0
|
|
1754
|
-
? { actionData: null }
|
|
1755
|
-
: { actionData }
|
|
1756
|
-
: {}),
|
|
1779
|
+
...(actionData !== undefined ? { actionData } : {}),
|
|
1757
1780
|
...(revalidatingFetchers.length > 0
|
|
1758
1781
|
? { fetchers: new Map(state.fetchers) }
|
|
1759
1782
|
: {}),
|
|
@@ -1786,7 +1809,7 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1786
1809
|
);
|
|
1787
1810
|
}
|
|
1788
1811
|
|
|
1789
|
-
let {
|
|
1812
|
+
let { loaderResults, fetcherResults } =
|
|
1790
1813
|
await callLoadersAndMaybeResolveData(
|
|
1791
1814
|
state.matches,
|
|
1792
1815
|
matches,
|
|
@@ -1811,7 +1834,7 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1811
1834
|
revalidatingFetchers.forEach((rf) => fetchControllers.delete(rf.key));
|
|
1812
1835
|
|
|
1813
1836
|
// If any loaders returned a redirect Response, start a new REPLACE navigation
|
|
1814
|
-
let redirect = findRedirect(
|
|
1837
|
+
let redirect = findRedirect([...loaderResults, ...fetcherResults]);
|
|
1815
1838
|
if (redirect) {
|
|
1816
1839
|
if (redirect.idx >= matchesToLoad.length) {
|
|
1817
1840
|
// If this redirect came from a fetcher make sure we mark it in
|
|
@@ -1821,7 +1844,9 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1821
1844
|
revalidatingFetchers[redirect.idx - matchesToLoad.length].key;
|
|
1822
1845
|
fetchRedirectIds.add(fetcherKey);
|
|
1823
1846
|
}
|
|
1824
|
-
await startRedirectNavigation(
|
|
1847
|
+
await startRedirectNavigation(request, redirect.result, {
|
|
1848
|
+
replace,
|
|
1849
|
+
});
|
|
1825
1850
|
return { shortCircuited: true };
|
|
1826
1851
|
}
|
|
1827
1852
|
|
|
@@ -1831,7 +1856,7 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1831
1856
|
matches,
|
|
1832
1857
|
matchesToLoad,
|
|
1833
1858
|
loaderResults,
|
|
1834
|
-
|
|
1859
|
+
pendingActionResult,
|
|
1835
1860
|
revalidatingFetchers,
|
|
1836
1861
|
fetcherResults,
|
|
1837
1862
|
activeDeferreds
|
|
@@ -1995,16 +2020,13 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1995
2020
|
fetchControllers.set(key, abortController);
|
|
1996
2021
|
|
|
1997
2022
|
let originatingLoadId = incrementingLoadId;
|
|
1998
|
-
let
|
|
2023
|
+
let actionResults = await callDataStrategy(
|
|
1999
2024
|
"action",
|
|
2000
2025
|
fetchRequest,
|
|
2001
|
-
match,
|
|
2002
|
-
requestMatches
|
|
2003
|
-
manifest,
|
|
2004
|
-
mapRouteProperties,
|
|
2005
|
-
basename,
|
|
2006
|
-
future.v7_relativeSplatPath
|
|
2026
|
+
[match],
|
|
2027
|
+
requestMatches
|
|
2007
2028
|
);
|
|
2029
|
+
let actionResult = actionResults[0];
|
|
2008
2030
|
|
|
2009
2031
|
if (fetchRequest.signal.aborted) {
|
|
2010
2032
|
// We can delete this so long as we weren't aborted by our own fetcher
|
|
@@ -2037,7 +2059,7 @@ export function createRouter(init: RouterInit): Router {
|
|
|
2037
2059
|
} else {
|
|
2038
2060
|
fetchRedirectIds.add(key);
|
|
2039
2061
|
updateFetcherState(key, getLoadingFetcher(submission));
|
|
2040
|
-
return startRedirectNavigation(
|
|
2062
|
+
return startRedirectNavigation(fetchRequest, actionResult, {
|
|
2041
2063
|
fetcherSubmission: submission,
|
|
2042
2064
|
});
|
|
2043
2065
|
}
|
|
@@ -2083,6 +2105,7 @@ export function createRouter(init: RouterInit): Router {
|
|
|
2083
2105
|
submission,
|
|
2084
2106
|
nextLocation,
|
|
2085
2107
|
false,
|
|
2108
|
+
future.unstable_skipActionErrorRevalidation,
|
|
2086
2109
|
isRevalidationRequired,
|
|
2087
2110
|
cancelledDeferredRoutes,
|
|
2088
2111
|
cancelledFetcherLoads,
|
|
@@ -2091,8 +2114,7 @@ export function createRouter(init: RouterInit): Router {
|
|
|
2091
2114
|
fetchRedirectIds,
|
|
2092
2115
|
routesToUse,
|
|
2093
2116
|
basename,
|
|
2094
|
-
|
|
2095
|
-
undefined // No need to send through errors since we short circuit above
|
|
2117
|
+
[match.route.id, actionResult]
|
|
2096
2118
|
);
|
|
2097
2119
|
|
|
2098
2120
|
// Put all revalidating fetchers into the loading state, except for the
|
|
@@ -2126,7 +2148,7 @@ export function createRouter(init: RouterInit): Router {
|
|
|
2126
2148
|
abortPendingFetchRevalidations
|
|
2127
2149
|
);
|
|
2128
2150
|
|
|
2129
|
-
let {
|
|
2151
|
+
let { loaderResults, fetcherResults } =
|
|
2130
2152
|
await callLoadersAndMaybeResolveData(
|
|
2131
2153
|
state.matches,
|
|
2132
2154
|
matches,
|
|
@@ -2148,7 +2170,7 @@ export function createRouter(init: RouterInit): Router {
|
|
|
2148
2170
|
fetchControllers.delete(key);
|
|
2149
2171
|
revalidatingFetchers.forEach((r) => fetchControllers.delete(r.key));
|
|
2150
2172
|
|
|
2151
|
-
let redirect = findRedirect(
|
|
2173
|
+
let redirect = findRedirect([...loaderResults, ...fetcherResults]);
|
|
2152
2174
|
if (redirect) {
|
|
2153
2175
|
if (redirect.idx >= matchesToLoad.length) {
|
|
2154
2176
|
// If this redirect came from a fetcher make sure we mark it in
|
|
@@ -2158,7 +2180,7 @@ export function createRouter(init: RouterInit): Router {
|
|
|
2158
2180
|
revalidatingFetchers[redirect.idx - matchesToLoad.length].key;
|
|
2159
2181
|
fetchRedirectIds.add(fetcherKey);
|
|
2160
2182
|
}
|
|
2161
|
-
return startRedirectNavigation(
|
|
2183
|
+
return startRedirectNavigation(revalidationRequest, redirect.result);
|
|
2162
2184
|
}
|
|
2163
2185
|
|
|
2164
2186
|
// Process and commit output from loaders
|
|
@@ -2246,16 +2268,13 @@ export function createRouter(init: RouterInit): Router {
|
|
|
2246
2268
|
fetchControllers.set(key, abortController);
|
|
2247
2269
|
|
|
2248
2270
|
let originatingLoadId = incrementingLoadId;
|
|
2249
|
-
let
|
|
2271
|
+
let results = await callDataStrategy(
|
|
2250
2272
|
"loader",
|
|
2251
2273
|
fetchRequest,
|
|
2252
|
-
match,
|
|
2253
|
-
matches
|
|
2254
|
-
manifest,
|
|
2255
|
-
mapRouteProperties,
|
|
2256
|
-
basename,
|
|
2257
|
-
future.v7_relativeSplatPath
|
|
2274
|
+
[match],
|
|
2275
|
+
matches
|
|
2258
2276
|
);
|
|
2277
|
+
let result = results[0];
|
|
2259
2278
|
|
|
2260
2279
|
// Deferred isn't supported for fetcher loads, await everything and treat it
|
|
2261
2280
|
// as a normal load. resolveDeferredData will return undefined if this
|
|
@@ -2293,7 +2312,7 @@ export function createRouter(init: RouterInit): Router {
|
|
|
2293
2312
|
return;
|
|
2294
2313
|
} else {
|
|
2295
2314
|
fetchRedirectIds.add(key);
|
|
2296
|
-
await startRedirectNavigation(
|
|
2315
|
+
await startRedirectNavigation(fetchRequest, result);
|
|
2297
2316
|
return;
|
|
2298
2317
|
}
|
|
2299
2318
|
}
|
|
@@ -2330,7 +2349,7 @@ export function createRouter(init: RouterInit): Router {
|
|
|
2330
2349
|
* the history action from the original navigation (PUSH or REPLACE).
|
|
2331
2350
|
*/
|
|
2332
2351
|
async function startRedirectNavigation(
|
|
2333
|
-
|
|
2352
|
+
request: Request,
|
|
2334
2353
|
redirect: RedirectResult,
|
|
2335
2354
|
{
|
|
2336
2355
|
submission,
|
|
@@ -2342,26 +2361,29 @@ export function createRouter(init: RouterInit): Router {
|
|
|
2342
2361
|
replace?: boolean;
|
|
2343
2362
|
} = {}
|
|
2344
2363
|
) {
|
|
2345
|
-
if (redirect.
|
|
2364
|
+
if (redirect.response.headers.has("X-Remix-Revalidate")) {
|
|
2346
2365
|
isRevalidationRequired = true;
|
|
2347
2366
|
}
|
|
2348
2367
|
|
|
2349
|
-
let
|
|
2368
|
+
let location = redirect.response.headers.get("Location");
|
|
2369
|
+
invariant(location, "Expected a Location header on the redirect Response");
|
|
2370
|
+
location = normalizeRedirectLocation(
|
|
2371
|
+
location,
|
|
2372
|
+
new URL(request.url),
|
|
2373
|
+
basename
|
|
2374
|
+
);
|
|
2375
|
+
let redirectLocation = createLocation(state.location, location, {
|
|
2350
2376
|
_isRedirect: true,
|
|
2351
2377
|
});
|
|
2352
|
-
invariant(
|
|
2353
|
-
redirectLocation,
|
|
2354
|
-
"Expected a location on the redirect navigation"
|
|
2355
|
-
);
|
|
2356
2378
|
|
|
2357
2379
|
if (isBrowser) {
|
|
2358
2380
|
let isDocumentReload = false;
|
|
2359
2381
|
|
|
2360
|
-
if (redirect.
|
|
2382
|
+
if (redirect.response.headers.has("X-Remix-Reload-Document")) {
|
|
2361
2383
|
// Hard reload if the response contained X-Remix-Reload-Document
|
|
2362
2384
|
isDocumentReload = true;
|
|
2363
|
-
} else if (ABSOLUTE_URL_REGEX.test(
|
|
2364
|
-
const url = init.history.createURL(
|
|
2385
|
+
} else if (ABSOLUTE_URL_REGEX.test(location)) {
|
|
2386
|
+
const url = init.history.createURL(location);
|
|
2365
2387
|
isDocumentReload =
|
|
2366
2388
|
// Hard reload if it's an absolute URL to a new origin
|
|
2367
2389
|
url.origin !== routerWindow.location.origin ||
|
|
@@ -2371,9 +2393,9 @@ export function createRouter(init: RouterInit): Router {
|
|
|
2371
2393
|
|
|
2372
2394
|
if (isDocumentReload) {
|
|
2373
2395
|
if (replace) {
|
|
2374
|
-
routerWindow.location.replace(
|
|
2396
|
+
routerWindow.location.replace(location);
|
|
2375
2397
|
} else {
|
|
2376
|
-
routerWindow.location.assign(
|
|
2398
|
+
routerWindow.location.assign(location);
|
|
2377
2399
|
}
|
|
2378
2400
|
return;
|
|
2379
2401
|
}
|
|
@@ -2404,14 +2426,14 @@ export function createRouter(init: RouterInit): Router {
|
|
|
2404
2426
|
// redirected location
|
|
2405
2427
|
let activeSubmission = submission || fetcherSubmission;
|
|
2406
2428
|
if (
|
|
2407
|
-
redirectPreserveMethodStatusCodes.has(redirect.status) &&
|
|
2429
|
+
redirectPreserveMethodStatusCodes.has(redirect.response.status) &&
|
|
2408
2430
|
activeSubmission &&
|
|
2409
2431
|
isMutationMethod(activeSubmission.formMethod)
|
|
2410
2432
|
) {
|
|
2411
2433
|
await startNavigation(redirectHistoryAction, redirectLocation, {
|
|
2412
2434
|
submission: {
|
|
2413
2435
|
...activeSubmission,
|
|
2414
|
-
formAction:
|
|
2436
|
+
formAction: location,
|
|
2415
2437
|
},
|
|
2416
2438
|
// Preserve this flag across redirects
|
|
2417
2439
|
preventScrollReset: pendingPreventScrollReset,
|
|
@@ -2433,6 +2455,55 @@ export function createRouter(init: RouterInit): Router {
|
|
|
2433
2455
|
}
|
|
2434
2456
|
}
|
|
2435
2457
|
|
|
2458
|
+
// Utility wrapper for calling dataStrategy client-side without having to
|
|
2459
|
+
// pass around the manifest, mapRouteProperties, etc.
|
|
2460
|
+
async function callDataStrategy(
|
|
2461
|
+
type: "loader" | "action",
|
|
2462
|
+
request: Request,
|
|
2463
|
+
matchesToLoad: AgnosticDataRouteMatch[],
|
|
2464
|
+
matches: AgnosticDataRouteMatch[]
|
|
2465
|
+
): Promise<DataResult[]> {
|
|
2466
|
+
try {
|
|
2467
|
+
let results = await callDataStrategyImpl(
|
|
2468
|
+
dataStrategyImpl,
|
|
2469
|
+
type,
|
|
2470
|
+
request,
|
|
2471
|
+
matchesToLoad,
|
|
2472
|
+
matches,
|
|
2473
|
+
manifest,
|
|
2474
|
+
mapRouteProperties
|
|
2475
|
+
);
|
|
2476
|
+
|
|
2477
|
+
return await Promise.all(
|
|
2478
|
+
results.map((result, i) => {
|
|
2479
|
+
if (isRedirectHandlerResult(result)) {
|
|
2480
|
+
let response = result.result as Response;
|
|
2481
|
+
return {
|
|
2482
|
+
type: ResultType.redirect,
|
|
2483
|
+
response: normalizeRelativeRoutingRedirectResponse(
|
|
2484
|
+
response,
|
|
2485
|
+
request,
|
|
2486
|
+
matchesToLoad[i].route.id,
|
|
2487
|
+
matches,
|
|
2488
|
+
basename,
|
|
2489
|
+
future.v7_relativeSplatPath
|
|
2490
|
+
),
|
|
2491
|
+
};
|
|
2492
|
+
}
|
|
2493
|
+
|
|
2494
|
+
return convertHandlerResultToDataResult(result);
|
|
2495
|
+
})
|
|
2496
|
+
);
|
|
2497
|
+
} catch (e) {
|
|
2498
|
+
// If the outer dataStrategy method throws, just return the error for all
|
|
2499
|
+
// matches - and it'll naturally bubble to the root
|
|
2500
|
+
return matchesToLoad.map(() => ({
|
|
2501
|
+
type: ResultType.error,
|
|
2502
|
+
error: e,
|
|
2503
|
+
}));
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2506
|
+
|
|
2436
2507
|
async function callLoadersAndMaybeResolveData(
|
|
2437
2508
|
currentMatches: AgnosticDataRouteMatch[],
|
|
2438
2509
|
matches: AgnosticDataRouteMatch[],
|
|
@@ -2440,45 +2511,33 @@ export function createRouter(init: RouterInit): Router {
|
|
|
2440
2511
|
fetchersToLoad: RevalidatingFetcher[],
|
|
2441
2512
|
request: Request
|
|
2442
2513
|
) {
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
...matchesToLoad.map((match) =>
|
|
2448
|
-
callLoaderOrAction(
|
|
2449
|
-
"loader",
|
|
2450
|
-
request,
|
|
2451
|
-
match,
|
|
2452
|
-
matches,
|
|
2453
|
-
manifest,
|
|
2454
|
-
mapRouteProperties,
|
|
2455
|
-
basename,
|
|
2456
|
-
future.v7_relativeSplatPath
|
|
2457
|
-
)
|
|
2458
|
-
),
|
|
2514
|
+
let [loaderResults, ...fetcherResults] = await Promise.all([
|
|
2515
|
+
matchesToLoad.length
|
|
2516
|
+
? callDataStrategy("loader", request, matchesToLoad, matches)
|
|
2517
|
+
: [],
|
|
2459
2518
|
...fetchersToLoad.map((f) => {
|
|
2460
2519
|
if (f.matches && f.match && f.controller) {
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
f.
|
|
2465
|
-
f.matches,
|
|
2466
|
-
manifest,
|
|
2467
|
-
mapRouteProperties,
|
|
2468
|
-
basename,
|
|
2469
|
-
future.v7_relativeSplatPath
|
|
2520
|
+
let fetcherRequest = createClientSideRequest(
|
|
2521
|
+
init.history,
|
|
2522
|
+
f.path,
|
|
2523
|
+
f.controller.signal
|
|
2470
2524
|
);
|
|
2525
|
+
return callDataStrategy(
|
|
2526
|
+
"loader",
|
|
2527
|
+
fetcherRequest,
|
|
2528
|
+
[f.match],
|
|
2529
|
+
f.matches
|
|
2530
|
+
).then((r) => r[0]);
|
|
2471
2531
|
} else {
|
|
2472
|
-
|
|
2532
|
+
return Promise.resolve<DataResult>({
|
|
2473
2533
|
type: ResultType.error,
|
|
2474
|
-
error: getInternalRouterError(404, {
|
|
2475
|
-
|
|
2476
|
-
|
|
2534
|
+
error: getInternalRouterError(404, {
|
|
2535
|
+
pathname: f.path,
|
|
2536
|
+
}),
|
|
2537
|
+
});
|
|
2477
2538
|
}
|
|
2478
2539
|
}),
|
|
2479
2540
|
]);
|
|
2480
|
-
let loaderResults = results.slice(0, matchesToLoad.length);
|
|
2481
|
-
let fetcherResults = results.slice(matchesToLoad.length);
|
|
2482
2541
|
|
|
2483
2542
|
await Promise.all([
|
|
2484
2543
|
resolveDeferredResults(
|
|
@@ -2498,7 +2557,10 @@ export function createRouter(init: RouterInit): Router {
|
|
|
2498
2557
|
),
|
|
2499
2558
|
]);
|
|
2500
2559
|
|
|
2501
|
-
return {
|
|
2560
|
+
return {
|
|
2561
|
+
loaderResults,
|
|
2562
|
+
fetcherResults,
|
|
2563
|
+
};
|
|
2502
2564
|
}
|
|
2503
2565
|
|
|
2504
2566
|
function interruptActiveLoads() {
|
|
@@ -2925,10 +2987,33 @@ export function createStaticHandler(
|
|
|
2925
2987
|
* redirect response is returned or thrown from any action/loader. We
|
|
2926
2988
|
* propagate that out and return the raw Response so the HTTP server can
|
|
2927
2989
|
* return it directly.
|
|
2990
|
+
*
|
|
2991
|
+
* - `opts.loadRouteIds` is an optional array of routeIds to run only a subset of
|
|
2992
|
+
* loaders during a query() call
|
|
2993
|
+
* - `opts.requestContext` is an optional server context that will be passed
|
|
2994
|
+
* to actions/loaders in the `context` parameter
|
|
2995
|
+
* - `opts.skipLoaderErrorBubbling` is an optional parameter that will prevent
|
|
2996
|
+
* the bubbling of errors which allows single-fetch-type implementations
|
|
2997
|
+
* where the client will handle the bubbling and we may need to return data
|
|
2998
|
+
* for the handling route
|
|
2999
|
+
* - `opts.skipLoaders` is an optional parameter that will prevent loaders
|
|
3000
|
+
* from running after an action
|
|
2928
3001
|
*/
|
|
2929
3002
|
async function query(
|
|
2930
3003
|
request: Request,
|
|
2931
|
-
{
|
|
3004
|
+
{
|
|
3005
|
+
loadRouteIds,
|
|
3006
|
+
requestContext,
|
|
3007
|
+
skipLoaderErrorBubbling,
|
|
3008
|
+
skipLoaders,
|
|
3009
|
+
unstable_dataStrategy,
|
|
3010
|
+
}: {
|
|
3011
|
+
loadRouteIds?: string[];
|
|
3012
|
+
requestContext?: unknown;
|
|
3013
|
+
skipLoaderErrorBubbling?: boolean;
|
|
3014
|
+
skipLoaders?: boolean;
|
|
3015
|
+
unstable_dataStrategy?: DataStrategyFunction;
|
|
3016
|
+
} = {}
|
|
2932
3017
|
): Promise<StaticHandlerContext | Response> {
|
|
2933
3018
|
let url = new URL(request.url);
|
|
2934
3019
|
let method = request.method;
|
|
@@ -2974,7 +3059,17 @@ export function createStaticHandler(
|
|
|
2974
3059
|
};
|
|
2975
3060
|
}
|
|
2976
3061
|
|
|
2977
|
-
let result = await queryImpl(
|
|
3062
|
+
let result = await queryImpl(
|
|
3063
|
+
request,
|
|
3064
|
+
location,
|
|
3065
|
+
matches,
|
|
3066
|
+
requestContext,
|
|
3067
|
+
unstable_dataStrategy || null,
|
|
3068
|
+
loadRouteIds || null,
|
|
3069
|
+
skipLoaderErrorBubbling === true,
|
|
3070
|
+
skipLoaders === true,
|
|
3071
|
+
null
|
|
3072
|
+
);
|
|
2978
3073
|
if (isResponse(result)) {
|
|
2979
3074
|
return result;
|
|
2980
3075
|
}
|
|
@@ -3004,6 +3099,12 @@ export function createStaticHandler(
|
|
|
3004
3099
|
* serialize the error as they see fit while including the proper response
|
|
3005
3100
|
* code. Examples here are 404 and 405 errors that occur prior to reaching
|
|
3006
3101
|
* any user-defined loaders.
|
|
3102
|
+
*
|
|
3103
|
+
* - `opts.routeId` allows you to specify the specific route handler to call.
|
|
3104
|
+
* If not provided the handler will determine the proper route by matching
|
|
3105
|
+
* against `request.url`
|
|
3106
|
+
* - `opts.requestContext` is an optional server context that will be passed
|
|
3107
|
+
* to actions/loaders in the `context` parameter
|
|
3007
3108
|
*/
|
|
3008
3109
|
async function queryRoute(
|
|
3009
3110
|
request: Request,
|
|
@@ -3043,6 +3144,10 @@ export function createStaticHandler(
|
|
|
3043
3144
|
location,
|
|
3044
3145
|
matches,
|
|
3045
3146
|
requestContext,
|
|
3147
|
+
null,
|
|
3148
|
+
null,
|
|
3149
|
+
false,
|
|
3150
|
+
false,
|
|
3046
3151
|
match
|
|
3047
3152
|
);
|
|
3048
3153
|
if (isResponse(result)) {
|
|
@@ -3079,7 +3184,11 @@ export function createStaticHandler(
|
|
|
3079
3184
|
location: Location,
|
|
3080
3185
|
matches: AgnosticDataRouteMatch[],
|
|
3081
3186
|
requestContext: unknown,
|
|
3082
|
-
|
|
3187
|
+
unstable_dataStrategy: DataStrategyFunction | null,
|
|
3188
|
+
loadRouteIds: string[] | null,
|
|
3189
|
+
skipLoaderErrorBubbling: boolean,
|
|
3190
|
+
skipLoaders: boolean,
|
|
3191
|
+
routeMatch: AgnosticDataRouteMatch | null
|
|
3083
3192
|
): Promise<Omit<StaticHandlerContext, "location" | "basename"> | Response> {
|
|
3084
3193
|
invariant(
|
|
3085
3194
|
request.signal,
|
|
@@ -3093,6 +3202,10 @@ export function createStaticHandler(
|
|
|
3093
3202
|
matches,
|
|
3094
3203
|
routeMatch || getTargetMatch(matches, location),
|
|
3095
3204
|
requestContext,
|
|
3205
|
+
unstable_dataStrategy,
|
|
3206
|
+
loadRouteIds,
|
|
3207
|
+
skipLoaderErrorBubbling,
|
|
3208
|
+
skipLoaders,
|
|
3096
3209
|
routeMatch != null
|
|
3097
3210
|
);
|
|
3098
3211
|
return result;
|
|
@@ -3102,6 +3215,9 @@ export function createStaticHandler(
|
|
|
3102
3215
|
request,
|
|
3103
3216
|
matches,
|
|
3104
3217
|
requestContext,
|
|
3218
|
+
unstable_dataStrategy,
|
|
3219
|
+
loadRouteIds,
|
|
3220
|
+
skipLoaderErrorBubbling,
|
|
3105
3221
|
routeMatch
|
|
3106
3222
|
);
|
|
3107
3223
|
return isResponse(result)
|
|
@@ -3112,14 +3228,14 @@ export function createStaticHandler(
|
|
|
3112
3228
|
actionHeaders: {},
|
|
3113
3229
|
};
|
|
3114
3230
|
} catch (e) {
|
|
3115
|
-
// If the user threw/returned a Response in callLoaderOrAction
|
|
3116
|
-
//
|
|
3117
|
-
//
|
|
3118
|
-
if (
|
|
3231
|
+
// If the user threw/returned a Response in callLoaderOrAction for a
|
|
3232
|
+
// `queryRoute` call, we throw the `HandlerResult` to bail out early
|
|
3233
|
+
// and then return or throw the raw Response here accordingly
|
|
3234
|
+
if (isHandlerResult(e) && isResponse(e.result)) {
|
|
3119
3235
|
if (e.type === ResultType.error) {
|
|
3120
|
-
throw e.
|
|
3236
|
+
throw e.result;
|
|
3121
3237
|
}
|
|
3122
|
-
return e.
|
|
3238
|
+
return e.result;
|
|
3123
3239
|
}
|
|
3124
3240
|
// Redirects are always returned since they don't propagate to catch
|
|
3125
3241
|
// boundaries
|
|
@@ -3135,6 +3251,10 @@ export function createStaticHandler(
|
|
|
3135
3251
|
matches: AgnosticDataRouteMatch[],
|
|
3136
3252
|
actionMatch: AgnosticDataRouteMatch,
|
|
3137
3253
|
requestContext: unknown,
|
|
3254
|
+
unstable_dataStrategy: DataStrategyFunction | null,
|
|
3255
|
+
loadRouteIds: string[] | null,
|
|
3256
|
+
skipLoaderErrorBubbling: boolean,
|
|
3257
|
+
skipLoaders: boolean,
|
|
3138
3258
|
isRouteRequest: boolean
|
|
3139
3259
|
): Promise<Omit<StaticHandlerContext, "location" | "basename"> | Response> {
|
|
3140
3260
|
let result: DataResult;
|
|
@@ -3153,17 +3273,16 @@ export function createStaticHandler(
|
|
|
3153
3273
|
error,
|
|
3154
3274
|
};
|
|
3155
3275
|
} else {
|
|
3156
|
-
|
|
3276
|
+
let results = await callDataStrategy(
|
|
3157
3277
|
"action",
|
|
3158
3278
|
request,
|
|
3159
|
-
actionMatch,
|
|
3279
|
+
[actionMatch],
|
|
3160
3280
|
matches,
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
future.v7_relativeSplatPath,
|
|
3165
|
-
{ isStaticRequest: true, isRouteRequest, requestContext }
|
|
3281
|
+
isRouteRequest,
|
|
3282
|
+
requestContext,
|
|
3283
|
+
unstable_dataStrategy
|
|
3166
3284
|
);
|
|
3285
|
+
result = results[0];
|
|
3167
3286
|
|
|
3168
3287
|
if (request.signal.aborted) {
|
|
3169
3288
|
throwStaticHandlerAbortedError(request, isRouteRequest, future);
|
|
@@ -3176,9 +3295,9 @@ export function createStaticHandler(
|
|
|
3176
3295
|
// can get back on the "throw all redirect responses" train here should
|
|
3177
3296
|
// this ever happen :/
|
|
3178
3297
|
throw new Response(null, {
|
|
3179
|
-
status: result.status,
|
|
3298
|
+
status: result.response.status,
|
|
3180
3299
|
headers: {
|
|
3181
|
-
Location: result.
|
|
3300
|
+
Location: result.response.headers.get("Location")!,
|
|
3182
3301
|
},
|
|
3183
3302
|
});
|
|
3184
3303
|
}
|
|
@@ -3215,51 +3334,102 @@ export function createStaticHandler(
|
|
|
3215
3334
|
};
|
|
3216
3335
|
}
|
|
3217
3336
|
|
|
3337
|
+
// Create a GET request for the loaders
|
|
3338
|
+
let loaderRequest = new Request(request.url, {
|
|
3339
|
+
headers: request.headers,
|
|
3340
|
+
redirect: request.redirect,
|
|
3341
|
+
signal: request.signal,
|
|
3342
|
+
});
|
|
3343
|
+
|
|
3218
3344
|
if (isErrorResult(result)) {
|
|
3219
3345
|
// Store off the pending error - we use it to determine which loaders
|
|
3220
3346
|
// to call and will commit it when we complete the navigation
|
|
3221
|
-
let boundaryMatch =
|
|
3347
|
+
let boundaryMatch = skipLoaderErrorBubbling
|
|
3348
|
+
? actionMatch
|
|
3349
|
+
: findNearestBoundary(matches, actionMatch.route.id);
|
|
3350
|
+
let statusCode = isRouteErrorResponse(result.error)
|
|
3351
|
+
? result.error.status
|
|
3352
|
+
: result.statusCode != null
|
|
3353
|
+
? result.statusCode
|
|
3354
|
+
: 500;
|
|
3355
|
+
let actionHeaders = {
|
|
3356
|
+
...(result.headers ? { [actionMatch.route.id]: result.headers } : {}),
|
|
3357
|
+
};
|
|
3358
|
+
|
|
3359
|
+
if (skipLoaders) {
|
|
3360
|
+
return {
|
|
3361
|
+
matches,
|
|
3362
|
+
loaderData: {},
|
|
3363
|
+
actionData: {},
|
|
3364
|
+
errors: {
|
|
3365
|
+
[boundaryMatch.route.id]: result.error,
|
|
3366
|
+
},
|
|
3367
|
+
statusCode,
|
|
3368
|
+
loaderHeaders: {},
|
|
3369
|
+
actionHeaders,
|
|
3370
|
+
activeDeferreds: null,
|
|
3371
|
+
};
|
|
3372
|
+
}
|
|
3373
|
+
|
|
3222
3374
|
let context = await loadRouteData(
|
|
3223
|
-
|
|
3375
|
+
loaderRequest,
|
|
3224
3376
|
matches,
|
|
3225
3377
|
requestContext,
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3378
|
+
unstable_dataStrategy,
|
|
3379
|
+
loadRouteIds,
|
|
3380
|
+
skipLoaderErrorBubbling,
|
|
3381
|
+
null,
|
|
3382
|
+
[boundaryMatch.route.id, result]
|
|
3230
3383
|
);
|
|
3231
3384
|
|
|
3232
3385
|
// action status codes take precedence over loader status codes
|
|
3233
3386
|
return {
|
|
3234
3387
|
...context,
|
|
3235
|
-
statusCode
|
|
3236
|
-
? result.error.status
|
|
3237
|
-
: 500,
|
|
3388
|
+
statusCode,
|
|
3238
3389
|
actionData: null,
|
|
3239
|
-
actionHeaders
|
|
3240
|
-
|
|
3390
|
+
actionHeaders,
|
|
3391
|
+
};
|
|
3392
|
+
}
|
|
3393
|
+
|
|
3394
|
+
let actionHeaders = result.headers
|
|
3395
|
+
? { [actionMatch.route.id]: result.headers }
|
|
3396
|
+
: {};
|
|
3397
|
+
|
|
3398
|
+
if (skipLoaders) {
|
|
3399
|
+
return {
|
|
3400
|
+
matches,
|
|
3401
|
+
loaderData: {},
|
|
3402
|
+
actionData: {
|
|
3403
|
+
[actionMatch.route.id]: result.data,
|
|
3241
3404
|
},
|
|
3405
|
+
errors: null,
|
|
3406
|
+
statusCode: result.statusCode || 200,
|
|
3407
|
+
loaderHeaders: {},
|
|
3408
|
+
actionHeaders,
|
|
3409
|
+
activeDeferreds: null,
|
|
3242
3410
|
};
|
|
3243
3411
|
}
|
|
3244
3412
|
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3413
|
+
let context = await loadRouteData(
|
|
3414
|
+
loaderRequest,
|
|
3415
|
+
matches,
|
|
3416
|
+
requestContext,
|
|
3417
|
+
unstable_dataStrategy,
|
|
3418
|
+
loadRouteIds,
|
|
3419
|
+
skipLoaderErrorBubbling,
|
|
3420
|
+
null
|
|
3421
|
+
);
|
|
3252
3422
|
|
|
3253
3423
|
return {
|
|
3254
3424
|
...context,
|
|
3255
|
-
// action status codes take precedence over loader status codes
|
|
3256
|
-
...(result.statusCode ? { statusCode: result.statusCode } : {}),
|
|
3257
3425
|
actionData: {
|
|
3258
3426
|
[actionMatch.route.id]: result.data,
|
|
3259
3427
|
},
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3428
|
+
// action status codes take precedence over loader status codes
|
|
3429
|
+
...(result.statusCode ? { statusCode: result.statusCode } : {}),
|
|
3430
|
+
actionHeaders: result.headers
|
|
3431
|
+
? { [actionMatch.route.id]: result.headers }
|
|
3432
|
+
: {},
|
|
3263
3433
|
};
|
|
3264
3434
|
}
|
|
3265
3435
|
|
|
@@ -3267,8 +3437,11 @@ export function createStaticHandler(
|
|
|
3267
3437
|
request: Request,
|
|
3268
3438
|
matches: AgnosticDataRouteMatch[],
|
|
3269
3439
|
requestContext: unknown,
|
|
3270
|
-
|
|
3271
|
-
|
|
3440
|
+
unstable_dataStrategy: DataStrategyFunction | null,
|
|
3441
|
+
loadRouteIds: string[] | null,
|
|
3442
|
+
skipLoaderErrorBubbling: boolean,
|
|
3443
|
+
routeMatch: AgnosticDataRouteMatch | null,
|
|
3444
|
+
pendingActionResult?: PendingActionResult
|
|
3272
3445
|
): Promise<
|
|
3273
3446
|
| Omit<
|
|
3274
3447
|
StaticHandlerContext,
|
|
@@ -3293,14 +3466,19 @@ export function createStaticHandler(
|
|
|
3293
3466
|
|
|
3294
3467
|
let requestMatches = routeMatch
|
|
3295
3468
|
? [routeMatch]
|
|
3296
|
-
:
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
);
|
|
3469
|
+
: pendingActionResult && isErrorResult(pendingActionResult[1])
|
|
3470
|
+
? getLoaderMatchesUntilBoundary(matches, pendingActionResult[0])
|
|
3471
|
+
: matches;
|
|
3300
3472
|
let matchesToLoad = requestMatches.filter(
|
|
3301
3473
|
(m) => m.route.loader || m.route.lazy
|
|
3302
3474
|
);
|
|
3303
3475
|
|
|
3476
|
+
if (loadRouteIds) {
|
|
3477
|
+
matchesToLoad = matchesToLoad.filter((m) =>
|
|
3478
|
+
loadRouteIds.includes(m.route.id)
|
|
3479
|
+
);
|
|
3480
|
+
}
|
|
3481
|
+
|
|
3304
3482
|
// Short circuit if we have no loaders to run (query())
|
|
3305
3483
|
if (matchesToLoad.length === 0) {
|
|
3306
3484
|
return {
|
|
@@ -3310,28 +3488,27 @@ export function createStaticHandler(
|
|
|
3310
3488
|
(acc, m) => Object.assign(acc, { [m.route.id]: null }),
|
|
3311
3489
|
{}
|
|
3312
3490
|
),
|
|
3313
|
-
errors:
|
|
3491
|
+
errors:
|
|
3492
|
+
pendingActionResult && isErrorResult(pendingActionResult[1])
|
|
3493
|
+
? {
|
|
3494
|
+
[pendingActionResult[0]]: pendingActionResult[1].error,
|
|
3495
|
+
}
|
|
3496
|
+
: null,
|
|
3314
3497
|
statusCode: 200,
|
|
3315
3498
|
loaderHeaders: {},
|
|
3316
3499
|
activeDeferreds: null,
|
|
3317
3500
|
};
|
|
3318
3501
|
}
|
|
3319
3502
|
|
|
3320
|
-
let results = await
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
basename,
|
|
3330
|
-
future.v7_relativeSplatPath,
|
|
3331
|
-
{ isStaticRequest: true, isRouteRequest, requestContext }
|
|
3332
|
-
)
|
|
3333
|
-
),
|
|
3334
|
-
]);
|
|
3503
|
+
let results = await callDataStrategy(
|
|
3504
|
+
"loader",
|
|
3505
|
+
request,
|
|
3506
|
+
matchesToLoad,
|
|
3507
|
+
matches,
|
|
3508
|
+
isRouteRequest,
|
|
3509
|
+
requestContext,
|
|
3510
|
+
unstable_dataStrategy
|
|
3511
|
+
);
|
|
3335
3512
|
|
|
3336
3513
|
if (request.signal.aborted) {
|
|
3337
3514
|
throwStaticHandlerAbortedError(request, isRouteRequest, future);
|
|
@@ -3343,8 +3520,9 @@ export function createStaticHandler(
|
|
|
3343
3520
|
matches,
|
|
3344
3521
|
matchesToLoad,
|
|
3345
3522
|
results,
|
|
3346
|
-
|
|
3347
|
-
activeDeferreds
|
|
3523
|
+
pendingActionResult,
|
|
3524
|
+
activeDeferreds,
|
|
3525
|
+
skipLoaderErrorBubbling
|
|
3348
3526
|
);
|
|
3349
3527
|
|
|
3350
3528
|
// Add a null for any non-loader matches for proper revalidation on the client
|
|
@@ -3367,6 +3545,53 @@ export function createStaticHandler(
|
|
|
3367
3545
|
};
|
|
3368
3546
|
}
|
|
3369
3547
|
|
|
3548
|
+
// Utility wrapper for calling dataStrategy server-side without having to
|
|
3549
|
+
// pass around the manifest, mapRouteProperties, etc.
|
|
3550
|
+
async function callDataStrategy(
|
|
3551
|
+
type: "loader" | "action",
|
|
3552
|
+
request: Request,
|
|
3553
|
+
matchesToLoad: AgnosticDataRouteMatch[],
|
|
3554
|
+
matches: AgnosticDataRouteMatch[],
|
|
3555
|
+
isRouteRequest: boolean,
|
|
3556
|
+
requestContext: unknown,
|
|
3557
|
+
unstable_dataStrategy: DataStrategyFunction | null
|
|
3558
|
+
): Promise<DataResult[]> {
|
|
3559
|
+
let results = await callDataStrategyImpl(
|
|
3560
|
+
unstable_dataStrategy || defaultDataStrategy,
|
|
3561
|
+
type,
|
|
3562
|
+
request,
|
|
3563
|
+
matchesToLoad,
|
|
3564
|
+
matches,
|
|
3565
|
+
manifest,
|
|
3566
|
+
mapRouteProperties,
|
|
3567
|
+
requestContext
|
|
3568
|
+
);
|
|
3569
|
+
|
|
3570
|
+
return await Promise.all(
|
|
3571
|
+
results.map((result, i) => {
|
|
3572
|
+
if (isRedirectHandlerResult(result)) {
|
|
3573
|
+
let response = result.result as Response;
|
|
3574
|
+
// Throw redirects and let the server handle them with an HTTP redirect
|
|
3575
|
+
throw normalizeRelativeRoutingRedirectResponse(
|
|
3576
|
+
response,
|
|
3577
|
+
request,
|
|
3578
|
+
matchesToLoad[i].route.id,
|
|
3579
|
+
matches,
|
|
3580
|
+
basename,
|
|
3581
|
+
future.v7_relativeSplatPath
|
|
3582
|
+
);
|
|
3583
|
+
}
|
|
3584
|
+
if (isResponse(result.result) && isRouteRequest) {
|
|
3585
|
+
// For SSR single-route requests, we want to hand Responses back
|
|
3586
|
+
// directly without unwrapping
|
|
3587
|
+
throw result;
|
|
3588
|
+
}
|
|
3589
|
+
|
|
3590
|
+
return convertHandlerResultToDataResult(result);
|
|
3591
|
+
})
|
|
3592
|
+
);
|
|
3593
|
+
}
|
|
3594
|
+
|
|
3370
3595
|
return {
|
|
3371
3596
|
dataRoutes,
|
|
3372
3597
|
query,
|
|
@@ -3643,7 +3868,7 @@ function normalizeNavigateOptions(
|
|
|
3643
3868
|
// render so we don't need to load them
|
|
3644
3869
|
function getLoaderMatchesUntilBoundary(
|
|
3645
3870
|
matches: AgnosticDataRouteMatch[],
|
|
3646
|
-
boundaryId
|
|
3871
|
+
boundaryId: string
|
|
3647
3872
|
) {
|
|
3648
3873
|
let boundaryMatches = matches;
|
|
3649
3874
|
if (boundaryId) {
|
|
@@ -3662,6 +3887,7 @@ function getMatchesToLoad(
|
|
|
3662
3887
|
submission: Submission | undefined,
|
|
3663
3888
|
location: Location,
|
|
3664
3889
|
isInitialLoad: boolean,
|
|
3890
|
+
skipActionErrorRevalidation: boolean,
|
|
3665
3891
|
isRevalidationRequired: boolean,
|
|
3666
3892
|
cancelledDeferredRoutes: string[],
|
|
3667
3893
|
cancelledFetcherLoads: string[],
|
|
@@ -3670,21 +3896,33 @@ function getMatchesToLoad(
|
|
|
3670
3896
|
fetchRedirectIds: Set<string>,
|
|
3671
3897
|
routesToUse: AgnosticDataRouteObject[],
|
|
3672
3898
|
basename: string | undefined,
|
|
3673
|
-
|
|
3674
|
-
pendingError?: RouteData
|
|
3899
|
+
pendingActionResult?: PendingActionResult
|
|
3675
3900
|
): [AgnosticDataRouteMatch[], RevalidatingFetcher[]] {
|
|
3676
|
-
let actionResult =
|
|
3677
|
-
?
|
|
3678
|
-
|
|
3679
|
-
|
|
3901
|
+
let actionResult = pendingActionResult
|
|
3902
|
+
? isErrorResult(pendingActionResult[1])
|
|
3903
|
+
? pendingActionResult[1].error
|
|
3904
|
+
: pendingActionResult[1].data
|
|
3680
3905
|
: undefined;
|
|
3681
|
-
|
|
3682
3906
|
let currentUrl = history.createURL(state.location);
|
|
3683
3907
|
let nextUrl = history.createURL(location);
|
|
3684
3908
|
|
|
3685
3909
|
// Pick navigation matches that are net-new or qualify for revalidation
|
|
3686
|
-
let boundaryId =
|
|
3687
|
-
|
|
3910
|
+
let boundaryId =
|
|
3911
|
+
pendingActionResult && isErrorResult(pendingActionResult[1])
|
|
3912
|
+
? pendingActionResult[0]
|
|
3913
|
+
: undefined;
|
|
3914
|
+
let boundaryMatches = boundaryId
|
|
3915
|
+
? getLoaderMatchesUntilBoundary(matches, boundaryId)
|
|
3916
|
+
: matches;
|
|
3917
|
+
|
|
3918
|
+
// Don't revalidate loaders by default after action 4xx/5xx responses
|
|
3919
|
+
// when the flag is enabled. They can still opt-into revalidation via
|
|
3920
|
+
// `shouldRevalidate` via `actionResult`
|
|
3921
|
+
let actionStatus = pendingActionResult
|
|
3922
|
+
? pendingActionResult[1].statusCode
|
|
3923
|
+
: undefined;
|
|
3924
|
+
let shouldSkipRevalidation =
|
|
3925
|
+
skipActionErrorRevalidation && actionStatus && actionStatus >= 400;
|
|
3688
3926
|
|
|
3689
3927
|
let navigationMatches = boundaryMatches.filter((match, index) => {
|
|
3690
3928
|
let { route } = match;
|
|
@@ -3698,7 +3936,7 @@ function getMatchesToLoad(
|
|
|
3698
3936
|
}
|
|
3699
3937
|
|
|
3700
3938
|
if (isInitialLoad) {
|
|
3701
|
-
if (route.loader.hydrate) {
|
|
3939
|
+
if (typeof route.loader !== "function" || route.loader.hydrate) {
|
|
3702
3940
|
return true;
|
|
3703
3941
|
}
|
|
3704
3942
|
return (
|
|
@@ -3730,15 +3968,16 @@ function getMatchesToLoad(
|
|
|
3730
3968
|
nextParams: nextRouteMatch.params,
|
|
3731
3969
|
...submission,
|
|
3732
3970
|
actionResult,
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
//
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3971
|
+
unstable_actionStatus: actionStatus,
|
|
3972
|
+
defaultShouldRevalidate: shouldSkipRevalidation
|
|
3973
|
+
? false
|
|
3974
|
+
: // Forced revalidation due to submission, useRevalidator, or X-Remix-Revalidate
|
|
3975
|
+
isRevalidationRequired ||
|
|
3976
|
+
currentUrl.pathname + currentUrl.search ===
|
|
3977
|
+
nextUrl.pathname + nextUrl.search ||
|
|
3978
|
+
// Search params affect all loaders
|
|
3979
|
+
currentUrl.search !== nextUrl.search ||
|
|
3980
|
+
isNewRouteInstance(currentRouteMatch, nextRouteMatch),
|
|
3742
3981
|
});
|
|
3743
3982
|
});
|
|
3744
3983
|
|
|
@@ -3808,7 +4047,10 @@ function getMatchesToLoad(
|
|
|
3808
4047
|
nextParams: matches[matches.length - 1].params,
|
|
3809
4048
|
...submission,
|
|
3810
4049
|
actionResult,
|
|
3811
|
-
|
|
4050
|
+
unstable_actionStatus: actionStatus,
|
|
4051
|
+
defaultShouldRevalidate: shouldSkipRevalidation
|
|
4052
|
+
? false
|
|
4053
|
+
: isRevalidationRequired,
|
|
3812
4054
|
});
|
|
3813
4055
|
}
|
|
3814
4056
|
|
|
@@ -3954,39 +4196,138 @@ async function loadLazyRouteModule(
|
|
|
3954
4196
|
});
|
|
3955
4197
|
}
|
|
3956
4198
|
|
|
4199
|
+
// Default implementation of `dataStrategy` which fetches all loaders in parallel
|
|
4200
|
+
function defaultDataStrategy(
|
|
4201
|
+
opts: DataStrategyFunctionArgs
|
|
4202
|
+
): ReturnType<DataStrategyFunction> {
|
|
4203
|
+
return Promise.all(opts.matches.map((m) => m.resolve()));
|
|
4204
|
+
}
|
|
4205
|
+
|
|
4206
|
+
async function callDataStrategyImpl(
|
|
4207
|
+
dataStrategyImpl: DataStrategyFunction,
|
|
4208
|
+
type: "loader" | "action",
|
|
4209
|
+
request: Request,
|
|
4210
|
+
matchesToLoad: AgnosticDataRouteMatch[],
|
|
4211
|
+
matches: AgnosticDataRouteMatch[],
|
|
4212
|
+
manifest: RouteManifest,
|
|
4213
|
+
mapRouteProperties: MapRoutePropertiesFunction,
|
|
4214
|
+
requestContext?: unknown
|
|
4215
|
+
): Promise<HandlerResult[]> {
|
|
4216
|
+
let routeIdsToLoad = matchesToLoad.reduce(
|
|
4217
|
+
(acc, m) => acc.add(m.route.id),
|
|
4218
|
+
new Set<string>()
|
|
4219
|
+
);
|
|
4220
|
+
let loadedMatches = new Set<string>();
|
|
4221
|
+
|
|
4222
|
+
// Send all matches here to allow for a middleware-type implementation.
|
|
4223
|
+
// handler will be a no-op for unneeded routes and we filter those results
|
|
4224
|
+
// back out below.
|
|
4225
|
+
let results = await dataStrategyImpl({
|
|
4226
|
+
matches: matches.map((match) => {
|
|
4227
|
+
let shouldLoad = routeIdsToLoad.has(match.route.id);
|
|
4228
|
+
// `resolve` encapsulates the route.lazy, executing the
|
|
4229
|
+
// loader/action, and mapping return values/thrown errors to a
|
|
4230
|
+
// HandlerResult. Users can pass a callback to take fine-grained control
|
|
4231
|
+
// over the execution of the loader/action
|
|
4232
|
+
let resolve: DataStrategyMatch["resolve"] = (handlerOverride) => {
|
|
4233
|
+
loadedMatches.add(match.route.id);
|
|
4234
|
+
return shouldLoad
|
|
4235
|
+
? callLoaderOrAction(
|
|
4236
|
+
type,
|
|
4237
|
+
request,
|
|
4238
|
+
match,
|
|
4239
|
+
manifest,
|
|
4240
|
+
mapRouteProperties,
|
|
4241
|
+
handlerOverride,
|
|
4242
|
+
requestContext
|
|
4243
|
+
)
|
|
4244
|
+
: Promise.resolve({ type: ResultType.data, result: undefined });
|
|
4245
|
+
};
|
|
4246
|
+
|
|
4247
|
+
return {
|
|
4248
|
+
...match,
|
|
4249
|
+
shouldLoad,
|
|
4250
|
+
resolve,
|
|
4251
|
+
};
|
|
4252
|
+
}),
|
|
4253
|
+
request,
|
|
4254
|
+
params: matches[0].params,
|
|
4255
|
+
context: requestContext,
|
|
4256
|
+
});
|
|
4257
|
+
|
|
4258
|
+
// Throw if any loadRoute implementations not called since they are what
|
|
4259
|
+
// ensures a route is fully loaded
|
|
4260
|
+
matches.forEach((m) =>
|
|
4261
|
+
invariant(
|
|
4262
|
+
loadedMatches.has(m.route.id),
|
|
4263
|
+
`\`match.resolve()\` was not called for route id "${m.route.id}". ` +
|
|
4264
|
+
"You must call `match.resolve()` on every match passed to " +
|
|
4265
|
+
"`dataStrategy` to ensure all routes are properly loaded."
|
|
4266
|
+
)
|
|
4267
|
+
);
|
|
4268
|
+
|
|
4269
|
+
// Filter out any middleware-only matches for which we didn't need to run handlers
|
|
4270
|
+
return results.filter((_, i) => routeIdsToLoad.has(matches[i].route.id));
|
|
4271
|
+
}
|
|
4272
|
+
|
|
4273
|
+
// Default logic for calling a loader/action is the user has no specified a dataStrategy
|
|
3957
4274
|
async function callLoaderOrAction(
|
|
3958
4275
|
type: "loader" | "action",
|
|
3959
4276
|
request: Request,
|
|
3960
4277
|
match: AgnosticDataRouteMatch,
|
|
3961
|
-
matches: AgnosticDataRouteMatch[],
|
|
3962
4278
|
manifest: RouteManifest,
|
|
3963
4279
|
mapRouteProperties: MapRoutePropertiesFunction,
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
isRouteRequest?: boolean;
|
|
3969
|
-
requestContext?: unknown;
|
|
3970
|
-
} = {}
|
|
3971
|
-
): Promise<DataResult> {
|
|
3972
|
-
let resultType;
|
|
3973
|
-
let result;
|
|
4280
|
+
handlerOverride: Parameters<DataStrategyMatch["resolve"]>[0],
|
|
4281
|
+
staticContext?: unknown
|
|
4282
|
+
): Promise<HandlerResult> {
|
|
4283
|
+
let result: HandlerResult;
|
|
3974
4284
|
let onReject: (() => void) | undefined;
|
|
3975
4285
|
|
|
3976
|
-
let runHandler = (
|
|
4286
|
+
let runHandler = (
|
|
4287
|
+
handler: AgnosticRouteObject["loader"] | AgnosticRouteObject["action"]
|
|
4288
|
+
): Promise<HandlerResult> => {
|
|
3977
4289
|
// Setup a promise we can race against so that abort signals short circuit
|
|
3978
4290
|
let reject: () => void;
|
|
3979
|
-
|
|
4291
|
+
// This will never resolve so safe to type it as Promise<HandlerResult> to
|
|
4292
|
+
// satisfy the function return value
|
|
4293
|
+
let abortPromise = new Promise<HandlerResult>((_, r) => (reject = r));
|
|
3980
4294
|
onReject = () => reject();
|
|
3981
4295
|
request.signal.addEventListener("abort", onReject);
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
4296
|
+
|
|
4297
|
+
let actualHandler = (ctx?: unknown) => {
|
|
4298
|
+
if (typeof handler !== "function") {
|
|
4299
|
+
return Promise.reject(
|
|
4300
|
+
new Error(
|
|
4301
|
+
`You cannot call the handler for a route which defines a boolean ` +
|
|
4302
|
+
`"${type}" [routeId: ${match.route.id}]`
|
|
4303
|
+
)
|
|
4304
|
+
);
|
|
4305
|
+
}
|
|
4306
|
+
return handler(
|
|
4307
|
+
{
|
|
4308
|
+
request,
|
|
4309
|
+
params: match.params,
|
|
4310
|
+
context: staticContext,
|
|
4311
|
+
},
|
|
4312
|
+
...(ctx !== undefined ? [ctx] : [])
|
|
4313
|
+
);
|
|
4314
|
+
};
|
|
4315
|
+
|
|
4316
|
+
let handlerPromise: Promise<HandlerResult>;
|
|
4317
|
+
if (handlerOverride) {
|
|
4318
|
+
handlerPromise = handlerOverride((ctx: unknown) => actualHandler(ctx));
|
|
4319
|
+
} else {
|
|
4320
|
+
handlerPromise = (async () => {
|
|
4321
|
+
try {
|
|
4322
|
+
let val = await actualHandler();
|
|
4323
|
+
return { type: "data", result: val };
|
|
4324
|
+
} catch (e) {
|
|
4325
|
+
return { type: "error", result: e };
|
|
4326
|
+
}
|
|
4327
|
+
})();
|
|
4328
|
+
}
|
|
4329
|
+
|
|
4330
|
+
return Promise.race([handlerPromise, abortPromise]);
|
|
3990
4331
|
};
|
|
3991
4332
|
|
|
3992
4333
|
try {
|
|
@@ -3996,7 +4337,7 @@ async function callLoaderOrAction(
|
|
|
3996
4337
|
if (handler) {
|
|
3997
4338
|
// Run statically defined handler in parallel with lazy()
|
|
3998
4339
|
let handlerError;
|
|
3999
|
-
let
|
|
4340
|
+
let [value] = await Promise.all([
|
|
4000
4341
|
// If the handler throws, don't let it immediately bubble out,
|
|
4001
4342
|
// since we need to let the lazy() execution finish so we know if this
|
|
4002
4343
|
// route has a boundary that can handle the error
|
|
@@ -4005,17 +4346,17 @@ async function callLoaderOrAction(
|
|
|
4005
4346
|
}),
|
|
4006
4347
|
loadLazyRouteModule(match.route, mapRouteProperties, manifest),
|
|
4007
4348
|
]);
|
|
4008
|
-
if (handlerError) {
|
|
4349
|
+
if (handlerError !== undefined) {
|
|
4009
4350
|
throw handlerError;
|
|
4010
4351
|
}
|
|
4011
|
-
result =
|
|
4352
|
+
result = value!;
|
|
4012
4353
|
} else {
|
|
4013
4354
|
// Load lazy route module, then run any returned handler
|
|
4014
4355
|
await loadLazyRouteModule(match.route, mapRouteProperties, manifest);
|
|
4015
4356
|
|
|
4016
4357
|
handler = match.route[type];
|
|
4017
4358
|
if (handler) {
|
|
4018
|
-
// Handler still
|
|
4359
|
+
// Handler still runs even if we got interrupted to maintain consistency
|
|
4019
4360
|
// with un-abortable behavior of handler execution on non-lazy or
|
|
4020
4361
|
// previously-lazy-loaded routes
|
|
4021
4362
|
result = await runHandler(handler);
|
|
@@ -4030,7 +4371,7 @@ async function callLoaderOrAction(
|
|
|
4030
4371
|
} else {
|
|
4031
4372
|
// lazy() route has no loader to run. Short circuit here so we don't
|
|
4032
4373
|
// hit the invariant below that errors on returning undefined.
|
|
4033
|
-
return { type: ResultType.data,
|
|
4374
|
+
return { type: ResultType.data, result: undefined };
|
|
4034
4375
|
}
|
|
4035
4376
|
}
|
|
4036
4377
|
} else if (!handler) {
|
|
@@ -4044,85 +4385,31 @@ async function callLoaderOrAction(
|
|
|
4044
4385
|
}
|
|
4045
4386
|
|
|
4046
4387
|
invariant(
|
|
4047
|
-
result !== undefined,
|
|
4388
|
+
result.result !== undefined,
|
|
4048
4389
|
`You defined ${type === "action" ? "an action" : "a loader"} for route ` +
|
|
4049
4390
|
`"${match.route.id}" but didn't return anything from your \`${type}\` ` +
|
|
4050
4391
|
`function. Please return a value or \`null\`.`
|
|
4051
4392
|
);
|
|
4052
4393
|
} catch (e) {
|
|
4053
|
-
|
|
4054
|
-
|
|
4394
|
+
// We should already be catching and converting normal handler executions to
|
|
4395
|
+
// HandlerResults and returning them, so anything that throws here is an
|
|
4396
|
+
// unexpected error we still need to wrap
|
|
4397
|
+
return { type: ResultType.error, result: e };
|
|
4055
4398
|
} finally {
|
|
4056
4399
|
if (onReject) {
|
|
4057
4400
|
request.signal.removeEventListener("abort", onReject);
|
|
4058
4401
|
}
|
|
4059
4402
|
}
|
|
4060
4403
|
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
// Process redirects
|
|
4065
|
-
if (redirectStatusCodes.has(status)) {
|
|
4066
|
-
let location = result.headers.get("Location");
|
|
4067
|
-
invariant(
|
|
4068
|
-
location,
|
|
4069
|
-
"Redirects returned/thrown from loaders/actions must have a Location header"
|
|
4070
|
-
);
|
|
4071
|
-
|
|
4072
|
-
// Support relative routing in internal redirects
|
|
4073
|
-
if (!ABSOLUTE_URL_REGEX.test(location)) {
|
|
4074
|
-
location = normalizeTo(
|
|
4075
|
-
new URL(request.url),
|
|
4076
|
-
matches.slice(0, matches.indexOf(match) + 1),
|
|
4077
|
-
basename,
|
|
4078
|
-
true,
|
|
4079
|
-
location,
|
|
4080
|
-
v7_relativeSplatPath
|
|
4081
|
-
);
|
|
4082
|
-
} else if (!opts.isStaticRequest) {
|
|
4083
|
-
// Strip off the protocol+origin for same-origin + same-basename absolute
|
|
4084
|
-
// redirects. If this is a static request, we can let it go back to the
|
|
4085
|
-
// browser as-is
|
|
4086
|
-
let currentUrl = new URL(request.url);
|
|
4087
|
-
let url = location.startsWith("//")
|
|
4088
|
-
? new URL(currentUrl.protocol + location)
|
|
4089
|
-
: new URL(location);
|
|
4090
|
-
let isSameBasename = stripBasename(url.pathname, basename) != null;
|
|
4091
|
-
if (url.origin === currentUrl.origin && isSameBasename) {
|
|
4092
|
-
location = url.pathname + url.search + url.hash;
|
|
4093
|
-
}
|
|
4094
|
-
}
|
|
4095
|
-
|
|
4096
|
-
// Don't process redirects in the router during static requests requests.
|
|
4097
|
-
// Instead, throw the Response and let the server handle it with an HTTP
|
|
4098
|
-
// redirect. We also update the Location header in place in this flow so
|
|
4099
|
-
// basename and relative routing is taken into account
|
|
4100
|
-
if (opts.isStaticRequest) {
|
|
4101
|
-
result.headers.set("Location", location);
|
|
4102
|
-
throw result;
|
|
4103
|
-
}
|
|
4104
|
-
|
|
4105
|
-
return {
|
|
4106
|
-
type: ResultType.redirect,
|
|
4107
|
-
status,
|
|
4108
|
-
location,
|
|
4109
|
-
revalidate: result.headers.get("X-Remix-Revalidate") !== null,
|
|
4110
|
-
reloadDocument: result.headers.get("X-Remix-Reload-Document") !== null,
|
|
4111
|
-
};
|
|
4112
|
-
}
|
|
4404
|
+
return result;
|
|
4405
|
+
}
|
|
4113
4406
|
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
let queryRouteResponse: QueryRouteResponse = {
|
|
4119
|
-
type:
|
|
4120
|
-
resultType === ResultType.error ? ResultType.error : ResultType.data,
|
|
4121
|
-
response: result,
|
|
4122
|
-
};
|
|
4123
|
-
throw queryRouteResponse;
|
|
4124
|
-
}
|
|
4407
|
+
async function convertHandlerResultToDataResult(
|
|
4408
|
+
handlerResult: HandlerResult
|
|
4409
|
+
): Promise<DataResult> {
|
|
4410
|
+
let { result, type, status } = handlerResult;
|
|
4125
4411
|
|
|
4412
|
+
if (isResponse(result)) {
|
|
4126
4413
|
let data: any;
|
|
4127
4414
|
|
|
4128
4415
|
try {
|
|
@@ -4142,10 +4429,11 @@ async function callLoaderOrAction(
|
|
|
4142
4429
|
return { type: ResultType.error, error: e };
|
|
4143
4430
|
}
|
|
4144
4431
|
|
|
4145
|
-
if (
|
|
4432
|
+
if (type === ResultType.error) {
|
|
4146
4433
|
return {
|
|
4147
|
-
type:
|
|
4148
|
-
error: new ErrorResponseImpl(status, result.statusText, data),
|
|
4434
|
+
type: ResultType.error,
|
|
4435
|
+
error: new ErrorResponseImpl(result.status, result.statusText, data),
|
|
4436
|
+
statusCode: result.status,
|
|
4149
4437
|
headers: result.headers,
|
|
4150
4438
|
};
|
|
4151
4439
|
}
|
|
@@ -4158,8 +4446,12 @@ async function callLoaderOrAction(
|
|
|
4158
4446
|
};
|
|
4159
4447
|
}
|
|
4160
4448
|
|
|
4161
|
-
if (
|
|
4162
|
-
return {
|
|
4449
|
+
if (type === ResultType.error) {
|
|
4450
|
+
return {
|
|
4451
|
+
type: ResultType.error,
|
|
4452
|
+
error: result,
|
|
4453
|
+
statusCode: isRouteErrorResponse(result) ? result.status : status,
|
|
4454
|
+
};
|
|
4163
4455
|
}
|
|
4164
4456
|
|
|
4165
4457
|
if (isDeferredData(result)) {
|
|
@@ -4171,7 +4463,60 @@ async function callLoaderOrAction(
|
|
|
4171
4463
|
};
|
|
4172
4464
|
}
|
|
4173
4465
|
|
|
4174
|
-
return { type: ResultType.data, data: result };
|
|
4466
|
+
return { type: ResultType.data, data: result, statusCode: status };
|
|
4467
|
+
}
|
|
4468
|
+
|
|
4469
|
+
// Support relative routing in internal redirects
|
|
4470
|
+
function normalizeRelativeRoutingRedirectResponse(
|
|
4471
|
+
response: Response,
|
|
4472
|
+
request: Request,
|
|
4473
|
+
routeId: string,
|
|
4474
|
+
matches: AgnosticDataRouteMatch[],
|
|
4475
|
+
basename: string,
|
|
4476
|
+
v7_relativeSplatPath: boolean
|
|
4477
|
+
) {
|
|
4478
|
+
let location = response.headers.get("Location");
|
|
4479
|
+
invariant(
|
|
4480
|
+
location,
|
|
4481
|
+
"Redirects returned/thrown from loaders/actions must have a Location header"
|
|
4482
|
+
);
|
|
4483
|
+
|
|
4484
|
+
if (!ABSOLUTE_URL_REGEX.test(location)) {
|
|
4485
|
+
let trimmedMatches = matches.slice(
|
|
4486
|
+
0,
|
|
4487
|
+
matches.findIndex((m) => m.route.id === routeId) + 1
|
|
4488
|
+
);
|
|
4489
|
+
location = normalizeTo(
|
|
4490
|
+
new URL(request.url),
|
|
4491
|
+
trimmedMatches,
|
|
4492
|
+
basename,
|
|
4493
|
+
true,
|
|
4494
|
+
location,
|
|
4495
|
+
v7_relativeSplatPath
|
|
4496
|
+
);
|
|
4497
|
+
response.headers.set("Location", location);
|
|
4498
|
+
}
|
|
4499
|
+
|
|
4500
|
+
return response;
|
|
4501
|
+
}
|
|
4502
|
+
|
|
4503
|
+
function normalizeRedirectLocation(
|
|
4504
|
+
location: string,
|
|
4505
|
+
currentUrl: URL,
|
|
4506
|
+
basename: string
|
|
4507
|
+
): string {
|
|
4508
|
+
if (ABSOLUTE_URL_REGEX.test(location)) {
|
|
4509
|
+
// Strip off the protocol+origin for same-origin + same-basename absolute redirects
|
|
4510
|
+
let normalizedLocation = location;
|
|
4511
|
+
let url = normalizedLocation.startsWith("//")
|
|
4512
|
+
? new URL(currentUrl.protocol + normalizedLocation)
|
|
4513
|
+
: new URL(normalizedLocation);
|
|
4514
|
+
let isSameBasename = stripBasename(url.pathname, basename) != null;
|
|
4515
|
+
if (url.origin === currentUrl.origin && isSameBasename) {
|
|
4516
|
+
return url.pathname + url.search + url.hash;
|
|
4517
|
+
}
|
|
4518
|
+
}
|
|
4519
|
+
return location;
|
|
4175
4520
|
}
|
|
4176
4521
|
|
|
4177
4522
|
// Utility method for creating the Request instances for loaders/actions during
|
|
@@ -4239,8 +4584,9 @@ function processRouteLoaderData(
|
|
|
4239
4584
|
matches: AgnosticDataRouteMatch[],
|
|
4240
4585
|
matchesToLoad: AgnosticDataRouteMatch[],
|
|
4241
4586
|
results: DataResult[],
|
|
4242
|
-
|
|
4243
|
-
activeDeferreds: Map<string, DeferredData
|
|
4587
|
+
pendingActionResult: PendingActionResult | undefined,
|
|
4588
|
+
activeDeferreds: Map<string, DeferredData>,
|
|
4589
|
+
skipLoaderErrorBubbling: boolean
|
|
4244
4590
|
): {
|
|
4245
4591
|
loaderData: RouterState["loaderData"];
|
|
4246
4592
|
errors: RouterState["errors"] | null;
|
|
@@ -4253,6 +4599,10 @@ function processRouteLoaderData(
|
|
|
4253
4599
|
let statusCode: number | undefined;
|
|
4254
4600
|
let foundError = false;
|
|
4255
4601
|
let loaderHeaders: Record<string, Headers> = {};
|
|
4602
|
+
let pendingError =
|
|
4603
|
+
pendingActionResult && isErrorResult(pendingActionResult[1])
|
|
4604
|
+
? pendingActionResult[1].error
|
|
4605
|
+
: undefined;
|
|
4256
4606
|
|
|
4257
4607
|
// Process loader results into state.loaderData/state.errors
|
|
4258
4608
|
results.forEach((result, index) => {
|
|
@@ -4262,23 +4612,27 @@ function processRouteLoaderData(
|
|
|
4262
4612
|
"Cannot handle redirect results in processLoaderData"
|
|
4263
4613
|
);
|
|
4264
4614
|
if (isErrorResult(result)) {
|
|
4265
|
-
// Look upwards from the matched route for the closest ancestor
|
|
4266
|
-
// error boundary, defaulting to the root match
|
|
4267
|
-
let boundaryMatch = findNearestBoundary(matches, id);
|
|
4268
4615
|
let error = result.error;
|
|
4269
4616
|
// If we have a pending action error, we report it at the highest-route
|
|
4270
4617
|
// that throws a loader error, and then clear it out to indicate that
|
|
4271
4618
|
// it was consumed
|
|
4272
|
-
if (pendingError) {
|
|
4273
|
-
error =
|
|
4619
|
+
if (pendingError !== undefined) {
|
|
4620
|
+
error = pendingError;
|
|
4274
4621
|
pendingError = undefined;
|
|
4275
4622
|
}
|
|
4276
4623
|
|
|
4277
4624
|
errors = errors || {};
|
|
4278
4625
|
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4626
|
+
if (skipLoaderErrorBubbling) {
|
|
4627
|
+
errors[id] = error;
|
|
4628
|
+
} else {
|
|
4629
|
+
// Look upwards from the matched route for the closest ancestor error
|
|
4630
|
+
// boundary, defaulting to the root match. Prefer higher error values
|
|
4631
|
+
// if lower errors bubble to the same boundary
|
|
4632
|
+
let boundaryMatch = findNearestBoundary(matches, id);
|
|
4633
|
+
if (errors[boundaryMatch.route.id] == null) {
|
|
4634
|
+
errors[boundaryMatch.route.id] = error;
|
|
4635
|
+
}
|
|
4282
4636
|
}
|
|
4283
4637
|
|
|
4284
4638
|
// Clear our any prior loaderData for the throwing route
|
|
@@ -4299,21 +4653,28 @@ function processRouteLoaderData(
|
|
|
4299
4653
|
if (isDeferredResult(result)) {
|
|
4300
4654
|
activeDeferreds.set(id, result.deferredData);
|
|
4301
4655
|
loaderData[id] = result.deferredData.data;
|
|
4656
|
+
// Error status codes always override success status codes, but if all
|
|
4657
|
+
// loaders are successful we take the deepest status code.
|
|
4658
|
+
if (
|
|
4659
|
+
result.statusCode != null &&
|
|
4660
|
+
result.statusCode !== 200 &&
|
|
4661
|
+
!foundError
|
|
4662
|
+
) {
|
|
4663
|
+
statusCode = result.statusCode;
|
|
4664
|
+
}
|
|
4665
|
+
if (result.headers) {
|
|
4666
|
+
loaderHeaders[id] = result.headers;
|
|
4667
|
+
}
|
|
4302
4668
|
} else {
|
|
4303
4669
|
loaderData[id] = result.data;
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
result.
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
) {
|
|
4313
|
-
statusCode = result.statusCode;
|
|
4314
|
-
}
|
|
4315
|
-
if (result.headers) {
|
|
4316
|
-
loaderHeaders[id] = result.headers;
|
|
4670
|
+
// Error status codes always override success status codes, but if all
|
|
4671
|
+
// loaders are successful we take the deepest status code.
|
|
4672
|
+
if (result.statusCode && result.statusCode !== 200 && !foundError) {
|
|
4673
|
+
statusCode = result.statusCode;
|
|
4674
|
+
}
|
|
4675
|
+
if (result.headers) {
|
|
4676
|
+
loaderHeaders[id] = result.headers;
|
|
4677
|
+
}
|
|
4317
4678
|
}
|
|
4318
4679
|
}
|
|
4319
4680
|
});
|
|
@@ -4321,9 +4682,9 @@ function processRouteLoaderData(
|
|
|
4321
4682
|
// If we didn't consume the pending action error (i.e., all loaders
|
|
4322
4683
|
// resolved), then consume it here. Also clear out any loaderData for the
|
|
4323
4684
|
// throwing route
|
|
4324
|
-
if (pendingError) {
|
|
4325
|
-
errors = pendingError;
|
|
4326
|
-
loaderData[
|
|
4685
|
+
if (pendingError !== undefined && pendingActionResult) {
|
|
4686
|
+
errors = { [pendingActionResult[0]]: pendingError };
|
|
4687
|
+
loaderData[pendingActionResult[0]] = undefined;
|
|
4327
4688
|
}
|
|
4328
4689
|
|
|
4329
4690
|
return {
|
|
@@ -4339,7 +4700,7 @@ function processLoaderData(
|
|
|
4339
4700
|
matches: AgnosticDataRouteMatch[],
|
|
4340
4701
|
matchesToLoad: AgnosticDataRouteMatch[],
|
|
4341
4702
|
results: DataResult[],
|
|
4342
|
-
|
|
4703
|
+
pendingActionResult: PendingActionResult | undefined,
|
|
4343
4704
|
revalidatingFetchers: RevalidatingFetcher[],
|
|
4344
4705
|
fetcherResults: DataResult[],
|
|
4345
4706
|
activeDeferreds: Map<string, DeferredData>
|
|
@@ -4351,8 +4712,9 @@ function processLoaderData(
|
|
|
4351
4712
|
matches,
|
|
4352
4713
|
matchesToLoad,
|
|
4353
4714
|
results,
|
|
4354
|
-
|
|
4355
|
-
activeDeferreds
|
|
4715
|
+
pendingActionResult,
|
|
4716
|
+
activeDeferreds,
|
|
4717
|
+
false // This method is only called client side so we always want to bubble
|
|
4356
4718
|
);
|
|
4357
4719
|
|
|
4358
4720
|
// Process results from our revalidating fetchers
|
|
@@ -4425,6 +4787,24 @@ function mergeLoaderData(
|
|
|
4425
4787
|
return mergedLoaderData;
|
|
4426
4788
|
}
|
|
4427
4789
|
|
|
4790
|
+
function getActionDataForCommit(
|
|
4791
|
+
pendingActionResult: PendingActionResult | undefined
|
|
4792
|
+
) {
|
|
4793
|
+
if (!pendingActionResult) {
|
|
4794
|
+
return {};
|
|
4795
|
+
}
|
|
4796
|
+
return isErrorResult(pendingActionResult[1])
|
|
4797
|
+
? {
|
|
4798
|
+
// Clear out prior actionData on errors
|
|
4799
|
+
actionData: {},
|
|
4800
|
+
}
|
|
4801
|
+
: {
|
|
4802
|
+
actionData: {
|
|
4803
|
+
[pendingActionResult[0]]: pendingActionResult[1].data,
|
|
4804
|
+
},
|
|
4805
|
+
};
|
|
4806
|
+
}
|
|
4807
|
+
|
|
4428
4808
|
// Find the nearest error boundary, looking upwards from the leaf route (or the
|
|
4429
4809
|
// route specified by routeId) for the closest ancestor error boundary,
|
|
4430
4810
|
// defaulting to the root match
|
|
@@ -4559,6 +4939,22 @@ function isHashChangeOnly(a: Location, b: Location): boolean {
|
|
|
4559
4939
|
return false;
|
|
4560
4940
|
}
|
|
4561
4941
|
|
|
4942
|
+
function isHandlerResult(result: unknown): result is HandlerResult {
|
|
4943
|
+
return (
|
|
4944
|
+
result != null &&
|
|
4945
|
+
typeof result === "object" &&
|
|
4946
|
+
"type" in result &&
|
|
4947
|
+
"result" in result &&
|
|
4948
|
+
(result.type === ResultType.data || result.type === ResultType.error)
|
|
4949
|
+
);
|
|
4950
|
+
}
|
|
4951
|
+
|
|
4952
|
+
function isRedirectHandlerResult(result: HandlerResult) {
|
|
4953
|
+
return (
|
|
4954
|
+
isResponse(result.result) && redirectStatusCodes.has(result.result.status)
|
|
4955
|
+
);
|
|
4956
|
+
}
|
|
4957
|
+
|
|
4562
4958
|
function isDeferredResult(result: DataResult): result is DeferredResult {
|
|
4563
4959
|
return result.type === ResultType.deferred;
|
|
4564
4960
|
}
|
|
@@ -4603,14 +4999,6 @@ function isRedirectResponse(result: any): result is Response {
|
|
|
4603
4999
|
return status >= 300 && status <= 399 && location != null;
|
|
4604
5000
|
}
|
|
4605
5001
|
|
|
4606
|
-
function isQueryRouteResponse(obj: any): obj is QueryRouteResponse {
|
|
4607
|
-
return (
|
|
4608
|
-
obj &&
|
|
4609
|
-
isResponse(obj.response) &&
|
|
4610
|
-
(obj.type === ResultType.data || obj.type === ResultType.error)
|
|
4611
|
-
);
|
|
4612
|
-
}
|
|
4613
|
-
|
|
4614
5002
|
function isValidMethod(method: string): method is FormMethod | V7_FormMethod {
|
|
4615
5003
|
return validRequestMethods.has(method.toLowerCase() as FormMethod);
|
|
4616
5004
|
}
|