@remix-run/router 1.15.3 → 1.16.0-pre.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -0
- package/dist/index.d.ts +1 -1
- package/dist/router.cjs.js +396 -247
- package/dist/router.cjs.js.map +1 -1
- package/dist/router.d.ts +5 -1
- package/dist/router.js +382 -235
- package/dist/router.js.map +1 -1
- package/dist/router.umd.js +396 -247
- 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 +677 -354
- 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,11 @@ export interface StaticHandler {
|
|
|
400
404
|
dataRoutes: AgnosticDataRouteObject[];
|
|
401
405
|
query(
|
|
402
406
|
request: Request,
|
|
403
|
-
opts?: {
|
|
407
|
+
opts?: {
|
|
408
|
+
requestContext?: unknown;
|
|
409
|
+
skipLoaderErrorBubbling?: boolean;
|
|
410
|
+
unstable_dataStrategy?: DataStrategyFunction;
|
|
411
|
+
}
|
|
404
412
|
): Promise<StaticHandlerContext | Response>;
|
|
405
413
|
queryRoute(
|
|
406
414
|
request: Request,
|
|
@@ -616,18 +624,14 @@ interface ShortCircuitable {
|
|
|
616
624
|
shortCircuited?: boolean;
|
|
617
625
|
}
|
|
618
626
|
|
|
627
|
+
type PendingActionResult = [string, SuccessResult | ErrorResult];
|
|
628
|
+
|
|
619
629
|
interface HandleActionResult extends ShortCircuitable {
|
|
620
630
|
/**
|
|
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
|
|
631
|
+
* Tuple for the returned or thrown value from the current action. The routeId
|
|
632
|
+
* is the action route for success and the bubbled boundary route for errors.
|
|
629
633
|
*/
|
|
630
|
-
|
|
634
|
+
pendingActionResult?: PendingActionResult;
|
|
631
635
|
}
|
|
632
636
|
|
|
633
637
|
interface HandleLoadersResult extends ShortCircuitable {
|
|
@@ -660,16 +664,6 @@ interface RevalidatingFetcher extends FetchLoadMatch {
|
|
|
660
664
|
controller: AbortController | null;
|
|
661
665
|
}
|
|
662
666
|
|
|
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
667
|
const validMutationMethodsArr: MutationFormMethod[] = [
|
|
674
668
|
"post",
|
|
675
669
|
"put",
|
|
@@ -776,6 +770,7 @@ export function createRouter(init: RouterInit): Router {
|
|
|
776
770
|
);
|
|
777
771
|
let inFlightDataRoutes: AgnosticDataRouteObject[] | undefined;
|
|
778
772
|
let basename = init.basename || "/";
|
|
773
|
+
let dataStrategyImpl = init.unstable_dataStrategy || defaultDataStrategy;
|
|
779
774
|
// Config driven behavior flags
|
|
780
775
|
let future: FutureConfig = {
|
|
781
776
|
v7_fetcherPersist: false,
|
|
@@ -783,6 +778,7 @@ export function createRouter(init: RouterInit): Router {
|
|
|
783
778
|
v7_partialHydration: false,
|
|
784
779
|
v7_prependBasename: false,
|
|
785
780
|
v7_relativeSplatPath: false,
|
|
781
|
+
unstable_skipActionErrorRevalidation: false,
|
|
786
782
|
...init.future,
|
|
787
783
|
};
|
|
788
784
|
// Cleanup function for history
|
|
@@ -835,9 +831,16 @@ export function createRouter(init: RouterInit): Router {
|
|
|
835
831
|
let errors = init.hydrationData ? init.hydrationData.errors : null;
|
|
836
832
|
let isRouteInitialized = (m: AgnosticDataRouteMatch) => {
|
|
837
833
|
// No loader, nothing to initialize
|
|
838
|
-
if (!m.route.loader)
|
|
834
|
+
if (!m.route.loader) {
|
|
835
|
+
return true;
|
|
836
|
+
}
|
|
839
837
|
// Explicitly opting-in to running on hydration
|
|
840
|
-
if (
|
|
838
|
+
if (
|
|
839
|
+
typeof m.route.loader === "function" &&
|
|
840
|
+
m.route.loader.hydrate === true
|
|
841
|
+
) {
|
|
842
|
+
return false;
|
|
843
|
+
}
|
|
841
844
|
// Otherwise, initialized if hydrated with data or an error
|
|
842
845
|
return (
|
|
843
846
|
(loaderData && loaderData[m.route.id] !== undefined) ||
|
|
@@ -1493,24 +1496,24 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1493
1496
|
pendingNavigationController.signal,
|
|
1494
1497
|
opts && opts.submission
|
|
1495
1498
|
);
|
|
1496
|
-
let
|
|
1497
|
-
let pendingError: RouteData | undefined;
|
|
1499
|
+
let pendingActionResult: PendingActionResult | undefined;
|
|
1498
1500
|
|
|
1499
1501
|
if (opts && opts.pendingError) {
|
|
1500
1502
|
// If we have a pendingError, it means the user attempted a GET submission
|
|
1501
1503
|
// with binary FormData so assign here and skip to handleLoaders. That
|
|
1502
1504
|
// way we handle calling loaders above the boundary etc. It's not really
|
|
1503
1505
|
// different from an actionError in that sense.
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1506
|
+
pendingActionResult = [
|
|
1507
|
+
findNearestBoundary(matches).route.id,
|
|
1508
|
+
{ type: ResultType.error, error: opts.pendingError },
|
|
1509
|
+
];
|
|
1507
1510
|
} else if (
|
|
1508
1511
|
opts &&
|
|
1509
1512
|
opts.submission &&
|
|
1510
1513
|
isMutationMethod(opts.submission.formMethod)
|
|
1511
1514
|
) {
|
|
1512
1515
|
// Call action if we received an action submission
|
|
1513
|
-
let
|
|
1516
|
+
let actionResult = await handleAction(
|
|
1514
1517
|
request,
|
|
1515
1518
|
location,
|
|
1516
1519
|
opts.submission,
|
|
@@ -1518,17 +1521,20 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1518
1521
|
{ replace: opts.replace, flushSync }
|
|
1519
1522
|
);
|
|
1520
1523
|
|
|
1521
|
-
if (
|
|
1524
|
+
if (actionResult.shortCircuited) {
|
|
1522
1525
|
return;
|
|
1523
1526
|
}
|
|
1524
1527
|
|
|
1525
|
-
|
|
1526
|
-
pendingError = actionOutput.pendingActionError;
|
|
1528
|
+
pendingActionResult = actionResult.pendingActionResult;
|
|
1527
1529
|
loadingNavigation = getLoadingNavigation(location, opts.submission);
|
|
1528
1530
|
flushSync = false;
|
|
1529
1531
|
|
|
1530
1532
|
// Create a GET request for the loaders
|
|
1531
|
-
request =
|
|
1533
|
+
request = createClientSideRequest(
|
|
1534
|
+
init.history,
|
|
1535
|
+
request.url,
|
|
1536
|
+
request.signal
|
|
1537
|
+
);
|
|
1532
1538
|
}
|
|
1533
1539
|
|
|
1534
1540
|
// Call loaders
|
|
@@ -1542,8 +1548,7 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1542
1548
|
opts && opts.replace,
|
|
1543
1549
|
opts && opts.initialHydration === true,
|
|
1544
1550
|
flushSync,
|
|
1545
|
-
|
|
1546
|
-
pendingError
|
|
1551
|
+
pendingActionResult
|
|
1547
1552
|
);
|
|
1548
1553
|
|
|
1549
1554
|
if (shortCircuited) {
|
|
@@ -1557,7 +1562,7 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1557
1562
|
|
|
1558
1563
|
completeNavigation(location, {
|
|
1559
1564
|
matches,
|
|
1560
|
-
...(
|
|
1565
|
+
...getActionDataForCommit(pendingActionResult),
|
|
1561
1566
|
loaderData,
|
|
1562
1567
|
errors,
|
|
1563
1568
|
});
|
|
@@ -1592,16 +1597,13 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1592
1597
|
}),
|
|
1593
1598
|
};
|
|
1594
1599
|
} else {
|
|
1595
|
-
|
|
1600
|
+
let results = await callDataStrategy(
|
|
1596
1601
|
"action",
|
|
1597
1602
|
request,
|
|
1598
|
-
actionMatch,
|
|
1599
|
-
matches
|
|
1600
|
-
manifest,
|
|
1601
|
-
mapRouteProperties,
|
|
1602
|
-
basename,
|
|
1603
|
-
future.v7_relativeSplatPath
|
|
1603
|
+
[actionMatch],
|
|
1604
|
+
matches
|
|
1604
1605
|
);
|
|
1606
|
+
result = results[0];
|
|
1605
1607
|
|
|
1606
1608
|
if (request.signal.aborted) {
|
|
1607
1609
|
return { shortCircuited: true };
|
|
@@ -1616,13 +1618,24 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1616
1618
|
// If the user didn't explicity indicate replace behavior, replace if
|
|
1617
1619
|
// we redirected to the exact same location we're currently at to avoid
|
|
1618
1620
|
// double back-buttons
|
|
1619
|
-
|
|
1620
|
-
result.
|
|
1621
|
+
let location = normalizeRedirectLocation(
|
|
1622
|
+
result.response.headers.get("Location")!,
|
|
1623
|
+
new URL(request.url),
|
|
1624
|
+
basename
|
|
1625
|
+
);
|
|
1626
|
+
replace = location === state.location.pathname + state.location.search;
|
|
1621
1627
|
}
|
|
1622
|
-
await startRedirectNavigation(
|
|
1628
|
+
await startRedirectNavigation(request, result, {
|
|
1629
|
+
submission,
|
|
1630
|
+
replace,
|
|
1631
|
+
});
|
|
1623
1632
|
return { shortCircuited: true };
|
|
1624
1633
|
}
|
|
1625
1634
|
|
|
1635
|
+
if (isDeferredResult(result)) {
|
|
1636
|
+
throw getInternalRouterError(400, { type: "defer-action" });
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1626
1639
|
if (isErrorResult(result)) {
|
|
1627
1640
|
// Store off the pending error - we use it to determine which loaders
|
|
1628
1641
|
// to call and will commit it when we complete the navigation
|
|
@@ -1637,18 +1650,12 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1637
1650
|
}
|
|
1638
1651
|
|
|
1639
1652
|
return {
|
|
1640
|
-
|
|
1641
|
-
pendingActionData: {},
|
|
1642
|
-
pendingActionError: { [boundaryMatch.route.id]: result.error },
|
|
1653
|
+
pendingActionResult: [boundaryMatch.route.id, result],
|
|
1643
1654
|
};
|
|
1644
1655
|
}
|
|
1645
1656
|
|
|
1646
|
-
if (isDeferredResult(result)) {
|
|
1647
|
-
throw getInternalRouterError(400, { type: "defer-action" });
|
|
1648
|
-
}
|
|
1649
|
-
|
|
1650
1657
|
return {
|
|
1651
|
-
|
|
1658
|
+
pendingActionResult: [actionMatch.route.id, result],
|
|
1652
1659
|
};
|
|
1653
1660
|
}
|
|
1654
1661
|
|
|
@@ -1664,8 +1671,7 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1664
1671
|
replace?: boolean,
|
|
1665
1672
|
initialHydration?: boolean,
|
|
1666
1673
|
flushSync?: boolean,
|
|
1667
|
-
|
|
1668
|
-
pendingError?: RouteData
|
|
1674
|
+
pendingActionResult?: PendingActionResult
|
|
1669
1675
|
): Promise<HandleLoadersResult> {
|
|
1670
1676
|
// Figure out the right navigation we want to use for data loading
|
|
1671
1677
|
let loadingNavigation =
|
|
@@ -1686,6 +1692,7 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1686
1692
|
activeSubmission,
|
|
1687
1693
|
location,
|
|
1688
1694
|
future.v7_partialHydration && initialHydration === true,
|
|
1695
|
+
future.unstable_skipActionErrorRevalidation,
|
|
1689
1696
|
isRevalidationRequired,
|
|
1690
1697
|
cancelledDeferredRoutes,
|
|
1691
1698
|
cancelledFetcherLoads,
|
|
@@ -1694,8 +1701,7 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1694
1701
|
fetchRedirectIds,
|
|
1695
1702
|
routesToUse,
|
|
1696
1703
|
basename,
|
|
1697
|
-
|
|
1698
|
-
pendingError
|
|
1704
|
+
pendingActionResult
|
|
1699
1705
|
);
|
|
1700
1706
|
|
|
1701
1707
|
// Cancel pending deferreds for no-longer-matched routes or routes we're
|
|
@@ -1718,8 +1724,11 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1718
1724
|
matches,
|
|
1719
1725
|
loaderData: {},
|
|
1720
1726
|
// Commit pending error if we're short circuiting
|
|
1721
|
-
errors:
|
|
1722
|
-
|
|
1727
|
+
errors:
|
|
1728
|
+
pendingActionResult && isErrorResult(pendingActionResult[1])
|
|
1729
|
+
? { [pendingActionResult[0]]: pendingActionResult[1].error }
|
|
1730
|
+
: null,
|
|
1731
|
+
...getActionDataForCommit(pendingActionResult),
|
|
1723
1732
|
...(updatedFetchers ? { fetchers: new Map(state.fetchers) } : {}),
|
|
1724
1733
|
},
|
|
1725
1734
|
{ flushSync }
|
|
@@ -1745,15 +1754,27 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1745
1754
|
);
|
|
1746
1755
|
state.fetchers.set(rf.key, revalidatingFetcher);
|
|
1747
1756
|
});
|
|
1748
|
-
|
|
1757
|
+
|
|
1758
|
+
let actionData: Record<string, RouteData> | null | undefined;
|
|
1759
|
+
if (pendingActionResult && !isErrorResult(pendingActionResult[1])) {
|
|
1760
|
+
// This is cast to `any` currently because `RouteData`uses any and it
|
|
1761
|
+
// would be a breaking change to use any.
|
|
1762
|
+
// TODO: v7 - change `RouteData` to use `unknown` instead of `any`
|
|
1763
|
+
actionData = {
|
|
1764
|
+
[pendingActionResult[0]]: pendingActionResult[1].data as any,
|
|
1765
|
+
};
|
|
1766
|
+
} else if (state.actionData) {
|
|
1767
|
+
if (Object.keys(state.actionData).length === 0) {
|
|
1768
|
+
actionData = null;
|
|
1769
|
+
} else {
|
|
1770
|
+
actionData = state.actionData;
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1749
1774
|
updateState(
|
|
1750
1775
|
{
|
|
1751
1776
|
navigation: loadingNavigation,
|
|
1752
|
-
...(actionData
|
|
1753
|
-
? Object.keys(actionData).length === 0
|
|
1754
|
-
? { actionData: null }
|
|
1755
|
-
: { actionData }
|
|
1756
|
-
: {}),
|
|
1777
|
+
...(actionData !== undefined ? { actionData } : {}),
|
|
1757
1778
|
...(revalidatingFetchers.length > 0
|
|
1758
1779
|
? { fetchers: new Map(state.fetchers) }
|
|
1759
1780
|
: {}),
|
|
@@ -1786,7 +1807,7 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1786
1807
|
);
|
|
1787
1808
|
}
|
|
1788
1809
|
|
|
1789
|
-
let {
|
|
1810
|
+
let { loaderResults, fetcherResults } =
|
|
1790
1811
|
await callLoadersAndMaybeResolveData(
|
|
1791
1812
|
state.matches,
|
|
1792
1813
|
matches,
|
|
@@ -1811,7 +1832,7 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1811
1832
|
revalidatingFetchers.forEach((rf) => fetchControllers.delete(rf.key));
|
|
1812
1833
|
|
|
1813
1834
|
// If any loaders returned a redirect Response, start a new REPLACE navigation
|
|
1814
|
-
let redirect = findRedirect(
|
|
1835
|
+
let redirect = findRedirect([...loaderResults, ...fetcherResults]);
|
|
1815
1836
|
if (redirect) {
|
|
1816
1837
|
if (redirect.idx >= matchesToLoad.length) {
|
|
1817
1838
|
// If this redirect came from a fetcher make sure we mark it in
|
|
@@ -1821,7 +1842,9 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1821
1842
|
revalidatingFetchers[redirect.idx - matchesToLoad.length].key;
|
|
1822
1843
|
fetchRedirectIds.add(fetcherKey);
|
|
1823
1844
|
}
|
|
1824
|
-
await startRedirectNavigation(
|
|
1845
|
+
await startRedirectNavigation(request, redirect.result, {
|
|
1846
|
+
replace,
|
|
1847
|
+
});
|
|
1825
1848
|
return { shortCircuited: true };
|
|
1826
1849
|
}
|
|
1827
1850
|
|
|
@@ -1831,7 +1854,7 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1831
1854
|
matches,
|
|
1832
1855
|
matchesToLoad,
|
|
1833
1856
|
loaderResults,
|
|
1834
|
-
|
|
1857
|
+
pendingActionResult,
|
|
1835
1858
|
revalidatingFetchers,
|
|
1836
1859
|
fetcherResults,
|
|
1837
1860
|
activeDeferreds
|
|
@@ -1995,16 +2018,13 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1995
2018
|
fetchControllers.set(key, abortController);
|
|
1996
2019
|
|
|
1997
2020
|
let originatingLoadId = incrementingLoadId;
|
|
1998
|
-
let
|
|
2021
|
+
let actionResults = await callDataStrategy(
|
|
1999
2022
|
"action",
|
|
2000
2023
|
fetchRequest,
|
|
2001
|
-
match,
|
|
2002
|
-
requestMatches
|
|
2003
|
-
manifest,
|
|
2004
|
-
mapRouteProperties,
|
|
2005
|
-
basename,
|
|
2006
|
-
future.v7_relativeSplatPath
|
|
2024
|
+
[match],
|
|
2025
|
+
requestMatches
|
|
2007
2026
|
);
|
|
2027
|
+
let actionResult = actionResults[0];
|
|
2008
2028
|
|
|
2009
2029
|
if (fetchRequest.signal.aborted) {
|
|
2010
2030
|
// We can delete this so long as we weren't aborted by our own fetcher
|
|
@@ -2037,7 +2057,7 @@ export function createRouter(init: RouterInit): Router {
|
|
|
2037
2057
|
} else {
|
|
2038
2058
|
fetchRedirectIds.add(key);
|
|
2039
2059
|
updateFetcherState(key, getLoadingFetcher(submission));
|
|
2040
|
-
return startRedirectNavigation(
|
|
2060
|
+
return startRedirectNavigation(fetchRequest, actionResult, {
|
|
2041
2061
|
fetcherSubmission: submission,
|
|
2042
2062
|
});
|
|
2043
2063
|
}
|
|
@@ -2083,6 +2103,7 @@ export function createRouter(init: RouterInit): Router {
|
|
|
2083
2103
|
submission,
|
|
2084
2104
|
nextLocation,
|
|
2085
2105
|
false,
|
|
2106
|
+
future.unstable_skipActionErrorRevalidation,
|
|
2086
2107
|
isRevalidationRequired,
|
|
2087
2108
|
cancelledDeferredRoutes,
|
|
2088
2109
|
cancelledFetcherLoads,
|
|
@@ -2091,8 +2112,7 @@ export function createRouter(init: RouterInit): Router {
|
|
|
2091
2112
|
fetchRedirectIds,
|
|
2092
2113
|
routesToUse,
|
|
2093
2114
|
basename,
|
|
2094
|
-
|
|
2095
|
-
undefined // No need to send through errors since we short circuit above
|
|
2115
|
+
[match.route.id, actionResult]
|
|
2096
2116
|
);
|
|
2097
2117
|
|
|
2098
2118
|
// Put all revalidating fetchers into the loading state, except for the
|
|
@@ -2126,7 +2146,7 @@ export function createRouter(init: RouterInit): Router {
|
|
|
2126
2146
|
abortPendingFetchRevalidations
|
|
2127
2147
|
);
|
|
2128
2148
|
|
|
2129
|
-
let {
|
|
2149
|
+
let { loaderResults, fetcherResults } =
|
|
2130
2150
|
await callLoadersAndMaybeResolveData(
|
|
2131
2151
|
state.matches,
|
|
2132
2152
|
matches,
|
|
@@ -2148,7 +2168,7 @@ export function createRouter(init: RouterInit): Router {
|
|
|
2148
2168
|
fetchControllers.delete(key);
|
|
2149
2169
|
revalidatingFetchers.forEach((r) => fetchControllers.delete(r.key));
|
|
2150
2170
|
|
|
2151
|
-
let redirect = findRedirect(
|
|
2171
|
+
let redirect = findRedirect([...loaderResults, ...fetcherResults]);
|
|
2152
2172
|
if (redirect) {
|
|
2153
2173
|
if (redirect.idx >= matchesToLoad.length) {
|
|
2154
2174
|
// If this redirect came from a fetcher make sure we mark it in
|
|
@@ -2158,7 +2178,7 @@ export function createRouter(init: RouterInit): Router {
|
|
|
2158
2178
|
revalidatingFetchers[redirect.idx - matchesToLoad.length].key;
|
|
2159
2179
|
fetchRedirectIds.add(fetcherKey);
|
|
2160
2180
|
}
|
|
2161
|
-
return startRedirectNavigation(
|
|
2181
|
+
return startRedirectNavigation(revalidationRequest, redirect.result);
|
|
2162
2182
|
}
|
|
2163
2183
|
|
|
2164
2184
|
// Process and commit output from loaders
|
|
@@ -2246,16 +2266,13 @@ export function createRouter(init: RouterInit): Router {
|
|
|
2246
2266
|
fetchControllers.set(key, abortController);
|
|
2247
2267
|
|
|
2248
2268
|
let originatingLoadId = incrementingLoadId;
|
|
2249
|
-
let
|
|
2269
|
+
let results = await callDataStrategy(
|
|
2250
2270
|
"loader",
|
|
2251
2271
|
fetchRequest,
|
|
2252
|
-
match,
|
|
2253
|
-
matches
|
|
2254
|
-
manifest,
|
|
2255
|
-
mapRouteProperties,
|
|
2256
|
-
basename,
|
|
2257
|
-
future.v7_relativeSplatPath
|
|
2272
|
+
[match],
|
|
2273
|
+
matches
|
|
2258
2274
|
);
|
|
2275
|
+
let result = results[0];
|
|
2259
2276
|
|
|
2260
2277
|
// Deferred isn't supported for fetcher loads, await everything and treat it
|
|
2261
2278
|
// as a normal load. resolveDeferredData will return undefined if this
|
|
@@ -2293,7 +2310,7 @@ export function createRouter(init: RouterInit): Router {
|
|
|
2293
2310
|
return;
|
|
2294
2311
|
} else {
|
|
2295
2312
|
fetchRedirectIds.add(key);
|
|
2296
|
-
await startRedirectNavigation(
|
|
2313
|
+
await startRedirectNavigation(fetchRequest, result);
|
|
2297
2314
|
return;
|
|
2298
2315
|
}
|
|
2299
2316
|
}
|
|
@@ -2330,7 +2347,7 @@ export function createRouter(init: RouterInit): Router {
|
|
|
2330
2347
|
* the history action from the original navigation (PUSH or REPLACE).
|
|
2331
2348
|
*/
|
|
2332
2349
|
async function startRedirectNavigation(
|
|
2333
|
-
|
|
2350
|
+
request: Request,
|
|
2334
2351
|
redirect: RedirectResult,
|
|
2335
2352
|
{
|
|
2336
2353
|
submission,
|
|
@@ -2342,26 +2359,29 @@ export function createRouter(init: RouterInit): Router {
|
|
|
2342
2359
|
replace?: boolean;
|
|
2343
2360
|
} = {}
|
|
2344
2361
|
) {
|
|
2345
|
-
if (redirect.
|
|
2362
|
+
if (redirect.response.headers.has("X-Remix-Revalidate")) {
|
|
2346
2363
|
isRevalidationRequired = true;
|
|
2347
2364
|
}
|
|
2348
2365
|
|
|
2349
|
-
let
|
|
2366
|
+
let location = redirect.response.headers.get("Location");
|
|
2367
|
+
invariant(location, "Expected a Location header on the redirect Response");
|
|
2368
|
+
location = normalizeRedirectLocation(
|
|
2369
|
+
location,
|
|
2370
|
+
new URL(request.url),
|
|
2371
|
+
basename
|
|
2372
|
+
);
|
|
2373
|
+
let redirectLocation = createLocation(state.location, location, {
|
|
2350
2374
|
_isRedirect: true,
|
|
2351
2375
|
});
|
|
2352
|
-
invariant(
|
|
2353
|
-
redirectLocation,
|
|
2354
|
-
"Expected a location on the redirect navigation"
|
|
2355
|
-
);
|
|
2356
2376
|
|
|
2357
2377
|
if (isBrowser) {
|
|
2358
2378
|
let isDocumentReload = false;
|
|
2359
2379
|
|
|
2360
|
-
if (redirect.
|
|
2380
|
+
if (redirect.response.headers.has("X-Remix-Reload-Document")) {
|
|
2361
2381
|
// Hard reload if the response contained X-Remix-Reload-Document
|
|
2362
2382
|
isDocumentReload = true;
|
|
2363
|
-
} else if (ABSOLUTE_URL_REGEX.test(
|
|
2364
|
-
const url = init.history.createURL(
|
|
2383
|
+
} else if (ABSOLUTE_URL_REGEX.test(location)) {
|
|
2384
|
+
const url = init.history.createURL(location);
|
|
2365
2385
|
isDocumentReload =
|
|
2366
2386
|
// Hard reload if it's an absolute URL to a new origin
|
|
2367
2387
|
url.origin !== routerWindow.location.origin ||
|
|
@@ -2371,9 +2391,9 @@ export function createRouter(init: RouterInit): Router {
|
|
|
2371
2391
|
|
|
2372
2392
|
if (isDocumentReload) {
|
|
2373
2393
|
if (replace) {
|
|
2374
|
-
routerWindow.location.replace(
|
|
2394
|
+
routerWindow.location.replace(location);
|
|
2375
2395
|
} else {
|
|
2376
|
-
routerWindow.location.assign(
|
|
2396
|
+
routerWindow.location.assign(location);
|
|
2377
2397
|
}
|
|
2378
2398
|
return;
|
|
2379
2399
|
}
|
|
@@ -2404,14 +2424,14 @@ export function createRouter(init: RouterInit): Router {
|
|
|
2404
2424
|
// redirected location
|
|
2405
2425
|
let activeSubmission = submission || fetcherSubmission;
|
|
2406
2426
|
if (
|
|
2407
|
-
redirectPreserveMethodStatusCodes.has(redirect.status) &&
|
|
2427
|
+
redirectPreserveMethodStatusCodes.has(redirect.response.status) &&
|
|
2408
2428
|
activeSubmission &&
|
|
2409
2429
|
isMutationMethod(activeSubmission.formMethod)
|
|
2410
2430
|
) {
|
|
2411
2431
|
await startNavigation(redirectHistoryAction, redirectLocation, {
|
|
2412
2432
|
submission: {
|
|
2413
2433
|
...activeSubmission,
|
|
2414
|
-
formAction:
|
|
2434
|
+
formAction: location,
|
|
2415
2435
|
},
|
|
2416
2436
|
// Preserve this flag across redirects
|
|
2417
2437
|
preventScrollReset: pendingPreventScrollReset,
|
|
@@ -2433,6 +2453,55 @@ export function createRouter(init: RouterInit): Router {
|
|
|
2433
2453
|
}
|
|
2434
2454
|
}
|
|
2435
2455
|
|
|
2456
|
+
// Utility wrapper for calling dataStrategy client-side without having to
|
|
2457
|
+
// pass around the manifest, mapRouteProperties, etc.
|
|
2458
|
+
async function callDataStrategy(
|
|
2459
|
+
type: "loader" | "action",
|
|
2460
|
+
request: Request,
|
|
2461
|
+
matchesToLoad: AgnosticDataRouteMatch[],
|
|
2462
|
+
matches: AgnosticDataRouteMatch[]
|
|
2463
|
+
): Promise<DataResult[]> {
|
|
2464
|
+
try {
|
|
2465
|
+
let results = await callDataStrategyImpl(
|
|
2466
|
+
dataStrategyImpl,
|
|
2467
|
+
type,
|
|
2468
|
+
request,
|
|
2469
|
+
matchesToLoad,
|
|
2470
|
+
matches,
|
|
2471
|
+
manifest,
|
|
2472
|
+
mapRouteProperties
|
|
2473
|
+
);
|
|
2474
|
+
|
|
2475
|
+
return await Promise.all(
|
|
2476
|
+
results.map((result, i) => {
|
|
2477
|
+
if (isRedirectHandlerResult(result)) {
|
|
2478
|
+
let response = result.result as Response;
|
|
2479
|
+
return {
|
|
2480
|
+
type: ResultType.redirect,
|
|
2481
|
+
response: normalizeRelativeRoutingRedirectResponse(
|
|
2482
|
+
response,
|
|
2483
|
+
request,
|
|
2484
|
+
matchesToLoad[i].route.id,
|
|
2485
|
+
matches,
|
|
2486
|
+
basename,
|
|
2487
|
+
future.v7_relativeSplatPath
|
|
2488
|
+
),
|
|
2489
|
+
};
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2492
|
+
return convertHandlerResultToDataResult(result);
|
|
2493
|
+
})
|
|
2494
|
+
);
|
|
2495
|
+
} catch (e) {
|
|
2496
|
+
// If the outer dataStrategy method throws, just return the error for all
|
|
2497
|
+
// matches - and it'll naturally bubble to the root
|
|
2498
|
+
return matchesToLoad.map(() => ({
|
|
2499
|
+
type: ResultType.error,
|
|
2500
|
+
error: e,
|
|
2501
|
+
}));
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
|
|
2436
2505
|
async function callLoadersAndMaybeResolveData(
|
|
2437
2506
|
currentMatches: AgnosticDataRouteMatch[],
|
|
2438
2507
|
matches: AgnosticDataRouteMatch[],
|
|
@@ -2440,45 +2509,33 @@ export function createRouter(init: RouterInit): Router {
|
|
|
2440
2509
|
fetchersToLoad: RevalidatingFetcher[],
|
|
2441
2510
|
request: Request
|
|
2442
2511
|
) {
|
|
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
|
-
),
|
|
2512
|
+
let [loaderResults, ...fetcherResults] = await Promise.all([
|
|
2513
|
+
matchesToLoad.length
|
|
2514
|
+
? callDataStrategy("loader", request, matchesToLoad, matches)
|
|
2515
|
+
: [],
|
|
2459
2516
|
...fetchersToLoad.map((f) => {
|
|
2460
2517
|
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
|
|
2518
|
+
let fetcherRequest = createClientSideRequest(
|
|
2519
|
+
init.history,
|
|
2520
|
+
f.path,
|
|
2521
|
+
f.controller.signal
|
|
2470
2522
|
);
|
|
2523
|
+
return callDataStrategy(
|
|
2524
|
+
"loader",
|
|
2525
|
+
fetcherRequest,
|
|
2526
|
+
[f.match],
|
|
2527
|
+
f.matches
|
|
2528
|
+
).then((r) => r[0]);
|
|
2471
2529
|
} else {
|
|
2472
|
-
|
|
2530
|
+
return Promise.resolve<DataResult>({
|
|
2473
2531
|
type: ResultType.error,
|
|
2474
|
-
error: getInternalRouterError(404, {
|
|
2475
|
-
|
|
2476
|
-
|
|
2532
|
+
error: getInternalRouterError(404, {
|
|
2533
|
+
pathname: f.path,
|
|
2534
|
+
}),
|
|
2535
|
+
});
|
|
2477
2536
|
}
|
|
2478
2537
|
}),
|
|
2479
2538
|
]);
|
|
2480
|
-
let loaderResults = results.slice(0, matchesToLoad.length);
|
|
2481
|
-
let fetcherResults = results.slice(matchesToLoad.length);
|
|
2482
2539
|
|
|
2483
2540
|
await Promise.all([
|
|
2484
2541
|
resolveDeferredResults(
|
|
@@ -2498,7 +2555,10 @@ export function createRouter(init: RouterInit): Router {
|
|
|
2498
2555
|
),
|
|
2499
2556
|
]);
|
|
2500
2557
|
|
|
2501
|
-
return {
|
|
2558
|
+
return {
|
|
2559
|
+
loaderResults,
|
|
2560
|
+
fetcherResults,
|
|
2561
|
+
};
|
|
2502
2562
|
}
|
|
2503
2563
|
|
|
2504
2564
|
function interruptActiveLoads() {
|
|
@@ -2925,10 +2985,25 @@ export function createStaticHandler(
|
|
|
2925
2985
|
* redirect response is returned or thrown from any action/loader. We
|
|
2926
2986
|
* propagate that out and return the raw Response so the HTTP server can
|
|
2927
2987
|
* return it directly.
|
|
2988
|
+
*
|
|
2989
|
+
* - `opts.requestContext` is an optional server context that will be passed
|
|
2990
|
+
* to actions/loaders in the `context` parameter
|
|
2991
|
+
* - `opts.skipLoaderErrorBubbling` is an optional parameter that will prevent
|
|
2992
|
+
* the bubbling of errors which allows single-fetch-type implementations
|
|
2993
|
+
* where the client will handle the bubbling and we may need to return data
|
|
2994
|
+
* for the handling route
|
|
2928
2995
|
*/
|
|
2929
2996
|
async function query(
|
|
2930
2997
|
request: Request,
|
|
2931
|
-
{
|
|
2998
|
+
{
|
|
2999
|
+
requestContext,
|
|
3000
|
+
skipLoaderErrorBubbling,
|
|
3001
|
+
unstable_dataStrategy,
|
|
3002
|
+
}: {
|
|
3003
|
+
requestContext?: unknown;
|
|
3004
|
+
skipLoaderErrorBubbling?: boolean;
|
|
3005
|
+
unstable_dataStrategy?: DataStrategyFunction;
|
|
3006
|
+
} = {}
|
|
2932
3007
|
): Promise<StaticHandlerContext | Response> {
|
|
2933
3008
|
let url = new URL(request.url);
|
|
2934
3009
|
let method = request.method;
|
|
@@ -2974,7 +3049,15 @@ export function createStaticHandler(
|
|
|
2974
3049
|
};
|
|
2975
3050
|
}
|
|
2976
3051
|
|
|
2977
|
-
let result = await queryImpl(
|
|
3052
|
+
let result = await queryImpl(
|
|
3053
|
+
request,
|
|
3054
|
+
location,
|
|
3055
|
+
matches,
|
|
3056
|
+
requestContext,
|
|
3057
|
+
unstable_dataStrategy || null,
|
|
3058
|
+
skipLoaderErrorBubbling === true,
|
|
3059
|
+
null
|
|
3060
|
+
);
|
|
2978
3061
|
if (isResponse(result)) {
|
|
2979
3062
|
return result;
|
|
2980
3063
|
}
|
|
@@ -3004,6 +3087,12 @@ export function createStaticHandler(
|
|
|
3004
3087
|
* serialize the error as they see fit while including the proper response
|
|
3005
3088
|
* code. Examples here are 404 and 405 errors that occur prior to reaching
|
|
3006
3089
|
* any user-defined loaders.
|
|
3090
|
+
*
|
|
3091
|
+
* - `opts.routeId` allows you to specify the specific route handler to call.
|
|
3092
|
+
* If not provided the handler will determine the proper route by matching
|
|
3093
|
+
* against `request.url`
|
|
3094
|
+
* - `opts.requestContext` is an optional server context that will be passed
|
|
3095
|
+
* to actions/loaders in the `context` parameter
|
|
3007
3096
|
*/
|
|
3008
3097
|
async function queryRoute(
|
|
3009
3098
|
request: Request,
|
|
@@ -3043,8 +3132,11 @@ export function createStaticHandler(
|
|
|
3043
3132
|
location,
|
|
3044
3133
|
matches,
|
|
3045
3134
|
requestContext,
|
|
3135
|
+
null,
|
|
3136
|
+
false,
|
|
3046
3137
|
match
|
|
3047
3138
|
);
|
|
3139
|
+
|
|
3048
3140
|
if (isResponse(result)) {
|
|
3049
3141
|
return result;
|
|
3050
3142
|
}
|
|
@@ -3079,7 +3171,9 @@ export function createStaticHandler(
|
|
|
3079
3171
|
location: Location,
|
|
3080
3172
|
matches: AgnosticDataRouteMatch[],
|
|
3081
3173
|
requestContext: unknown,
|
|
3082
|
-
|
|
3174
|
+
unstable_dataStrategy: DataStrategyFunction | null,
|
|
3175
|
+
skipLoaderErrorBubbling: boolean,
|
|
3176
|
+
routeMatch: AgnosticDataRouteMatch | null
|
|
3083
3177
|
): Promise<Omit<StaticHandlerContext, "location" | "basename"> | Response> {
|
|
3084
3178
|
invariant(
|
|
3085
3179
|
request.signal,
|
|
@@ -3093,6 +3187,8 @@ export function createStaticHandler(
|
|
|
3093
3187
|
matches,
|
|
3094
3188
|
routeMatch || getTargetMatch(matches, location),
|
|
3095
3189
|
requestContext,
|
|
3190
|
+
unstable_dataStrategy,
|
|
3191
|
+
skipLoaderErrorBubbling,
|
|
3096
3192
|
routeMatch != null
|
|
3097
3193
|
);
|
|
3098
3194
|
return result;
|
|
@@ -3102,6 +3198,8 @@ export function createStaticHandler(
|
|
|
3102
3198
|
request,
|
|
3103
3199
|
matches,
|
|
3104
3200
|
requestContext,
|
|
3201
|
+
unstable_dataStrategy,
|
|
3202
|
+
skipLoaderErrorBubbling,
|
|
3105
3203
|
routeMatch
|
|
3106
3204
|
);
|
|
3107
3205
|
return isResponse(result)
|
|
@@ -3112,14 +3210,14 @@ export function createStaticHandler(
|
|
|
3112
3210
|
actionHeaders: {},
|
|
3113
3211
|
};
|
|
3114
3212
|
} catch (e) {
|
|
3115
|
-
// If the user threw/returned a Response in callLoaderOrAction
|
|
3116
|
-
//
|
|
3117
|
-
//
|
|
3118
|
-
if (
|
|
3213
|
+
// If the user threw/returned a Response in callLoaderOrAction for a
|
|
3214
|
+
// `queryRoute` call, we throw the `HandlerResult` to bail out early
|
|
3215
|
+
// and then return or throw the raw Response here accordingly
|
|
3216
|
+
if (isHandlerResult(e) && isResponse(e.result)) {
|
|
3119
3217
|
if (e.type === ResultType.error) {
|
|
3120
|
-
throw e.
|
|
3218
|
+
throw e.result;
|
|
3121
3219
|
}
|
|
3122
|
-
return e.
|
|
3220
|
+
return e.result;
|
|
3123
3221
|
}
|
|
3124
3222
|
// Redirects are always returned since they don't propagate to catch
|
|
3125
3223
|
// boundaries
|
|
@@ -3135,6 +3233,8 @@ export function createStaticHandler(
|
|
|
3135
3233
|
matches: AgnosticDataRouteMatch[],
|
|
3136
3234
|
actionMatch: AgnosticDataRouteMatch,
|
|
3137
3235
|
requestContext: unknown,
|
|
3236
|
+
unstable_dataStrategy: DataStrategyFunction | null,
|
|
3237
|
+
skipLoaderErrorBubbling: boolean,
|
|
3138
3238
|
isRouteRequest: boolean
|
|
3139
3239
|
): Promise<Omit<StaticHandlerContext, "location" | "basename"> | Response> {
|
|
3140
3240
|
let result: DataResult;
|
|
@@ -3153,17 +3253,16 @@ export function createStaticHandler(
|
|
|
3153
3253
|
error,
|
|
3154
3254
|
};
|
|
3155
3255
|
} else {
|
|
3156
|
-
|
|
3256
|
+
let results = await callDataStrategy(
|
|
3157
3257
|
"action",
|
|
3158
3258
|
request,
|
|
3159
|
-
actionMatch,
|
|
3259
|
+
[actionMatch],
|
|
3160
3260
|
matches,
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
future.v7_relativeSplatPath,
|
|
3165
|
-
{ isStaticRequest: true, isRouteRequest, requestContext }
|
|
3261
|
+
isRouteRequest,
|
|
3262
|
+
requestContext,
|
|
3263
|
+
unstable_dataStrategy
|
|
3166
3264
|
);
|
|
3265
|
+
result = results[0];
|
|
3167
3266
|
|
|
3168
3267
|
if (request.signal.aborted) {
|
|
3169
3268
|
throwStaticHandlerAbortedError(request, isRouteRequest, future);
|
|
@@ -3176,9 +3275,9 @@ export function createStaticHandler(
|
|
|
3176
3275
|
// can get back on the "throw all redirect responses" train here should
|
|
3177
3276
|
// this ever happen :/
|
|
3178
3277
|
throw new Response(null, {
|
|
3179
|
-
status: result.status,
|
|
3278
|
+
status: result.response.status,
|
|
3180
3279
|
headers: {
|
|
3181
|
-
Location: result.
|
|
3280
|
+
Location: result.response.headers.get("Location")!,
|
|
3182
3281
|
},
|
|
3183
3282
|
});
|
|
3184
3283
|
}
|
|
@@ -3215,18 +3314,28 @@ export function createStaticHandler(
|
|
|
3215
3314
|
};
|
|
3216
3315
|
}
|
|
3217
3316
|
|
|
3317
|
+
// Create a GET request for the loaders
|
|
3318
|
+
let loaderRequest = new Request(request.url, {
|
|
3319
|
+
headers: request.headers,
|
|
3320
|
+
redirect: request.redirect,
|
|
3321
|
+
signal: request.signal,
|
|
3322
|
+
});
|
|
3323
|
+
|
|
3218
3324
|
if (isErrorResult(result)) {
|
|
3219
3325
|
// Store off the pending error - we use it to determine which loaders
|
|
3220
3326
|
// to call and will commit it when we complete the navigation
|
|
3221
|
-
let boundaryMatch =
|
|
3327
|
+
let boundaryMatch = skipLoaderErrorBubbling
|
|
3328
|
+
? actionMatch
|
|
3329
|
+
: findNearestBoundary(matches, actionMatch.route.id);
|
|
3330
|
+
|
|
3222
3331
|
let context = await loadRouteData(
|
|
3223
|
-
|
|
3332
|
+
loaderRequest,
|
|
3224
3333
|
matches,
|
|
3225
3334
|
requestContext,
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3335
|
+
unstable_dataStrategy,
|
|
3336
|
+
skipLoaderErrorBubbling,
|
|
3337
|
+
null,
|
|
3338
|
+
[boundaryMatch.route.id, result]
|
|
3230
3339
|
);
|
|
3231
3340
|
|
|
3232
3341
|
// action status codes take precedence over loader status codes
|
|
@@ -3234,6 +3343,8 @@ export function createStaticHandler(
|
|
|
3234
3343
|
...context,
|
|
3235
3344
|
statusCode: isRouteErrorResponse(result.error)
|
|
3236
3345
|
? result.error.status
|
|
3346
|
+
: result.statusCode != null
|
|
3347
|
+
? result.statusCode
|
|
3237
3348
|
: 500,
|
|
3238
3349
|
actionData: null,
|
|
3239
3350
|
actionHeaders: {
|
|
@@ -3242,24 +3353,25 @@ export function createStaticHandler(
|
|
|
3242
3353
|
};
|
|
3243
3354
|
}
|
|
3244
3355
|
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3356
|
+
let context = await loadRouteData(
|
|
3357
|
+
loaderRequest,
|
|
3358
|
+
matches,
|
|
3359
|
+
requestContext,
|
|
3360
|
+
unstable_dataStrategy,
|
|
3361
|
+
skipLoaderErrorBubbling,
|
|
3362
|
+
null
|
|
3363
|
+
);
|
|
3252
3364
|
|
|
3253
3365
|
return {
|
|
3254
3366
|
...context,
|
|
3255
|
-
// action status codes take precedence over loader status codes
|
|
3256
|
-
...(result.statusCode ? { statusCode: result.statusCode } : {}),
|
|
3257
3367
|
actionData: {
|
|
3258
3368
|
[actionMatch.route.id]: result.data,
|
|
3259
3369
|
},
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3370
|
+
// action status codes take precedence over loader status codes
|
|
3371
|
+
...(result.statusCode ? { statusCode: result.statusCode } : {}),
|
|
3372
|
+
actionHeaders: result.headers
|
|
3373
|
+
? { [actionMatch.route.id]: result.headers }
|
|
3374
|
+
: {},
|
|
3263
3375
|
};
|
|
3264
3376
|
}
|
|
3265
3377
|
|
|
@@ -3267,8 +3379,10 @@ export function createStaticHandler(
|
|
|
3267
3379
|
request: Request,
|
|
3268
3380
|
matches: AgnosticDataRouteMatch[],
|
|
3269
3381
|
requestContext: unknown,
|
|
3270
|
-
|
|
3271
|
-
|
|
3382
|
+
unstable_dataStrategy: DataStrategyFunction | null,
|
|
3383
|
+
skipLoaderErrorBubbling: boolean,
|
|
3384
|
+
routeMatch: AgnosticDataRouteMatch | null,
|
|
3385
|
+
pendingActionResult?: PendingActionResult
|
|
3272
3386
|
): Promise<
|
|
3273
3387
|
| Omit<
|
|
3274
3388
|
StaticHandlerContext,
|
|
@@ -3293,10 +3407,9 @@ export function createStaticHandler(
|
|
|
3293
3407
|
|
|
3294
3408
|
let requestMatches = routeMatch
|
|
3295
3409
|
? [routeMatch]
|
|
3296
|
-
:
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
);
|
|
3410
|
+
: pendingActionResult && isErrorResult(pendingActionResult[1])
|
|
3411
|
+
? getLoaderMatchesUntilBoundary(matches, pendingActionResult[0])
|
|
3412
|
+
: matches;
|
|
3300
3413
|
let matchesToLoad = requestMatches.filter(
|
|
3301
3414
|
(m) => m.route.loader || m.route.lazy
|
|
3302
3415
|
);
|
|
@@ -3310,28 +3423,27 @@ export function createStaticHandler(
|
|
|
3310
3423
|
(acc, m) => Object.assign(acc, { [m.route.id]: null }),
|
|
3311
3424
|
{}
|
|
3312
3425
|
),
|
|
3313
|
-
errors:
|
|
3426
|
+
errors:
|
|
3427
|
+
pendingActionResult && isErrorResult(pendingActionResult[1])
|
|
3428
|
+
? {
|
|
3429
|
+
[pendingActionResult[0]]: pendingActionResult[1].error,
|
|
3430
|
+
}
|
|
3431
|
+
: null,
|
|
3314
3432
|
statusCode: 200,
|
|
3315
3433
|
loaderHeaders: {},
|
|
3316
3434
|
activeDeferreds: null,
|
|
3317
3435
|
};
|
|
3318
3436
|
}
|
|
3319
3437
|
|
|
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
|
-
]);
|
|
3438
|
+
let results = await callDataStrategy(
|
|
3439
|
+
"loader",
|
|
3440
|
+
request,
|
|
3441
|
+
matchesToLoad,
|
|
3442
|
+
matches,
|
|
3443
|
+
isRouteRequest,
|
|
3444
|
+
requestContext,
|
|
3445
|
+
unstable_dataStrategy
|
|
3446
|
+
);
|
|
3335
3447
|
|
|
3336
3448
|
if (request.signal.aborted) {
|
|
3337
3449
|
throwStaticHandlerAbortedError(request, isRouteRequest, future);
|
|
@@ -3343,8 +3455,9 @@ export function createStaticHandler(
|
|
|
3343
3455
|
matches,
|
|
3344
3456
|
matchesToLoad,
|
|
3345
3457
|
results,
|
|
3346
|
-
|
|
3347
|
-
activeDeferreds
|
|
3458
|
+
pendingActionResult,
|
|
3459
|
+
activeDeferreds,
|
|
3460
|
+
skipLoaderErrorBubbling
|
|
3348
3461
|
);
|
|
3349
3462
|
|
|
3350
3463
|
// Add a null for any non-loader matches for proper revalidation on the client
|
|
@@ -3367,6 +3480,53 @@ export function createStaticHandler(
|
|
|
3367
3480
|
};
|
|
3368
3481
|
}
|
|
3369
3482
|
|
|
3483
|
+
// Utility wrapper for calling dataStrategy server-side without having to
|
|
3484
|
+
// pass around the manifest, mapRouteProperties, etc.
|
|
3485
|
+
async function callDataStrategy(
|
|
3486
|
+
type: "loader" | "action",
|
|
3487
|
+
request: Request,
|
|
3488
|
+
matchesToLoad: AgnosticDataRouteMatch[],
|
|
3489
|
+
matches: AgnosticDataRouteMatch[],
|
|
3490
|
+
isRouteRequest: boolean,
|
|
3491
|
+
requestContext: unknown,
|
|
3492
|
+
unstable_dataStrategy: DataStrategyFunction | null
|
|
3493
|
+
): Promise<DataResult[]> {
|
|
3494
|
+
let results = await callDataStrategyImpl(
|
|
3495
|
+
unstable_dataStrategy || defaultDataStrategy,
|
|
3496
|
+
type,
|
|
3497
|
+
request,
|
|
3498
|
+
matchesToLoad,
|
|
3499
|
+
matches,
|
|
3500
|
+
manifest,
|
|
3501
|
+
mapRouteProperties,
|
|
3502
|
+
requestContext
|
|
3503
|
+
);
|
|
3504
|
+
|
|
3505
|
+
return await Promise.all(
|
|
3506
|
+
results.map((result, i) => {
|
|
3507
|
+
if (isRedirectHandlerResult(result)) {
|
|
3508
|
+
let response = result.result as Response;
|
|
3509
|
+
// Throw redirects and let the server handle them with an HTTP redirect
|
|
3510
|
+
throw normalizeRelativeRoutingRedirectResponse(
|
|
3511
|
+
response,
|
|
3512
|
+
request,
|
|
3513
|
+
matchesToLoad[i].route.id,
|
|
3514
|
+
matches,
|
|
3515
|
+
basename,
|
|
3516
|
+
future.v7_relativeSplatPath
|
|
3517
|
+
);
|
|
3518
|
+
}
|
|
3519
|
+
if (isResponse(result.result) && isRouteRequest) {
|
|
3520
|
+
// For SSR single-route requests, we want to hand Responses back
|
|
3521
|
+
// directly without unwrapping
|
|
3522
|
+
throw result;
|
|
3523
|
+
}
|
|
3524
|
+
|
|
3525
|
+
return convertHandlerResultToDataResult(result);
|
|
3526
|
+
})
|
|
3527
|
+
);
|
|
3528
|
+
}
|
|
3529
|
+
|
|
3370
3530
|
return {
|
|
3371
3531
|
dataRoutes,
|
|
3372
3532
|
query,
|
|
@@ -3643,7 +3803,7 @@ function normalizeNavigateOptions(
|
|
|
3643
3803
|
// render so we don't need to load them
|
|
3644
3804
|
function getLoaderMatchesUntilBoundary(
|
|
3645
3805
|
matches: AgnosticDataRouteMatch[],
|
|
3646
|
-
boundaryId
|
|
3806
|
+
boundaryId: string
|
|
3647
3807
|
) {
|
|
3648
3808
|
let boundaryMatches = matches;
|
|
3649
3809
|
if (boundaryId) {
|
|
@@ -3662,6 +3822,7 @@ function getMatchesToLoad(
|
|
|
3662
3822
|
submission: Submission | undefined,
|
|
3663
3823
|
location: Location,
|
|
3664
3824
|
isInitialLoad: boolean,
|
|
3825
|
+
skipActionErrorRevalidation: boolean,
|
|
3665
3826
|
isRevalidationRequired: boolean,
|
|
3666
3827
|
cancelledDeferredRoutes: string[],
|
|
3667
3828
|
cancelledFetcherLoads: string[],
|
|
@@ -3670,21 +3831,33 @@ function getMatchesToLoad(
|
|
|
3670
3831
|
fetchRedirectIds: Set<string>,
|
|
3671
3832
|
routesToUse: AgnosticDataRouteObject[],
|
|
3672
3833
|
basename: string | undefined,
|
|
3673
|
-
|
|
3674
|
-
pendingError?: RouteData
|
|
3834
|
+
pendingActionResult?: PendingActionResult
|
|
3675
3835
|
): [AgnosticDataRouteMatch[], RevalidatingFetcher[]] {
|
|
3676
|
-
let actionResult =
|
|
3677
|
-
?
|
|
3678
|
-
|
|
3679
|
-
|
|
3836
|
+
let actionResult = pendingActionResult
|
|
3837
|
+
? isErrorResult(pendingActionResult[1])
|
|
3838
|
+
? pendingActionResult[1].error
|
|
3839
|
+
: pendingActionResult[1].data
|
|
3680
3840
|
: undefined;
|
|
3681
|
-
|
|
3682
3841
|
let currentUrl = history.createURL(state.location);
|
|
3683
3842
|
let nextUrl = history.createURL(location);
|
|
3684
3843
|
|
|
3685
3844
|
// Pick navigation matches that are net-new or qualify for revalidation
|
|
3686
|
-
let boundaryId =
|
|
3687
|
-
|
|
3845
|
+
let boundaryId =
|
|
3846
|
+
pendingActionResult && isErrorResult(pendingActionResult[1])
|
|
3847
|
+
? pendingActionResult[0]
|
|
3848
|
+
: undefined;
|
|
3849
|
+
let boundaryMatches = boundaryId
|
|
3850
|
+
? getLoaderMatchesUntilBoundary(matches, boundaryId)
|
|
3851
|
+
: matches;
|
|
3852
|
+
|
|
3853
|
+
// Don't revalidate loaders by default after action 4xx/5xx responses
|
|
3854
|
+
// when the flag is enabled. They can still opt-into revalidation via
|
|
3855
|
+
// `shouldRevalidate` via `actionResult`
|
|
3856
|
+
let actionStatus = pendingActionResult
|
|
3857
|
+
? pendingActionResult[1].statusCode
|
|
3858
|
+
: undefined;
|
|
3859
|
+
let shouldSkipRevalidation =
|
|
3860
|
+
skipActionErrorRevalidation && actionStatus && actionStatus >= 400;
|
|
3688
3861
|
|
|
3689
3862
|
let navigationMatches = boundaryMatches.filter((match, index) => {
|
|
3690
3863
|
let { route } = match;
|
|
@@ -3698,7 +3871,7 @@ function getMatchesToLoad(
|
|
|
3698
3871
|
}
|
|
3699
3872
|
|
|
3700
3873
|
if (isInitialLoad) {
|
|
3701
|
-
if (route.loader.hydrate) {
|
|
3874
|
+
if (typeof route.loader !== "function" || route.loader.hydrate) {
|
|
3702
3875
|
return true;
|
|
3703
3876
|
}
|
|
3704
3877
|
return (
|
|
@@ -3730,15 +3903,16 @@ function getMatchesToLoad(
|
|
|
3730
3903
|
nextParams: nextRouteMatch.params,
|
|
3731
3904
|
...submission,
|
|
3732
3905
|
actionResult,
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
//
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3906
|
+
unstable_actionStatus: actionStatus,
|
|
3907
|
+
defaultShouldRevalidate: shouldSkipRevalidation
|
|
3908
|
+
? false
|
|
3909
|
+
: // Forced revalidation due to submission, useRevalidator, or X-Remix-Revalidate
|
|
3910
|
+
isRevalidationRequired ||
|
|
3911
|
+
currentUrl.pathname + currentUrl.search ===
|
|
3912
|
+
nextUrl.pathname + nextUrl.search ||
|
|
3913
|
+
// Search params affect all loaders
|
|
3914
|
+
currentUrl.search !== nextUrl.search ||
|
|
3915
|
+
isNewRouteInstance(currentRouteMatch, nextRouteMatch),
|
|
3742
3916
|
});
|
|
3743
3917
|
});
|
|
3744
3918
|
|
|
@@ -3808,7 +3982,10 @@ function getMatchesToLoad(
|
|
|
3808
3982
|
nextParams: matches[matches.length - 1].params,
|
|
3809
3983
|
...submission,
|
|
3810
3984
|
actionResult,
|
|
3811
|
-
|
|
3985
|
+
unstable_actionStatus: actionStatus,
|
|
3986
|
+
defaultShouldRevalidate: shouldSkipRevalidation
|
|
3987
|
+
? false
|
|
3988
|
+
: isRevalidationRequired,
|
|
3812
3989
|
});
|
|
3813
3990
|
}
|
|
3814
3991
|
|
|
@@ -3954,39 +4131,138 @@ async function loadLazyRouteModule(
|
|
|
3954
4131
|
});
|
|
3955
4132
|
}
|
|
3956
4133
|
|
|
4134
|
+
// Default implementation of `dataStrategy` which fetches all loaders in parallel
|
|
4135
|
+
function defaultDataStrategy(
|
|
4136
|
+
opts: DataStrategyFunctionArgs
|
|
4137
|
+
): ReturnType<DataStrategyFunction> {
|
|
4138
|
+
return Promise.all(opts.matches.map((m) => m.resolve()));
|
|
4139
|
+
}
|
|
4140
|
+
|
|
4141
|
+
async function callDataStrategyImpl(
|
|
4142
|
+
dataStrategyImpl: DataStrategyFunction,
|
|
4143
|
+
type: "loader" | "action",
|
|
4144
|
+
request: Request,
|
|
4145
|
+
matchesToLoad: AgnosticDataRouteMatch[],
|
|
4146
|
+
matches: AgnosticDataRouteMatch[],
|
|
4147
|
+
manifest: RouteManifest,
|
|
4148
|
+
mapRouteProperties: MapRoutePropertiesFunction,
|
|
4149
|
+
requestContext?: unknown
|
|
4150
|
+
): Promise<HandlerResult[]> {
|
|
4151
|
+
let routeIdsToLoad = matchesToLoad.reduce(
|
|
4152
|
+
(acc, m) => acc.add(m.route.id),
|
|
4153
|
+
new Set<string>()
|
|
4154
|
+
);
|
|
4155
|
+
let loadedMatches = new Set<string>();
|
|
4156
|
+
|
|
4157
|
+
// Send all matches here to allow for a middleware-type implementation.
|
|
4158
|
+
// handler will be a no-op for unneeded routes and we filter those results
|
|
4159
|
+
// back out below.
|
|
4160
|
+
let results = await dataStrategyImpl({
|
|
4161
|
+
matches: matches.map((match) => {
|
|
4162
|
+
let shouldLoad = routeIdsToLoad.has(match.route.id);
|
|
4163
|
+
// `resolve` encapsulates the route.lazy, executing the
|
|
4164
|
+
// loader/action, and mapping return values/thrown errors to a
|
|
4165
|
+
// HandlerResult. Users can pass a callback to take fine-grained control
|
|
4166
|
+
// over the execution of the loader/action
|
|
4167
|
+
let resolve: DataStrategyMatch["resolve"] = (handlerOverride) => {
|
|
4168
|
+
loadedMatches.add(match.route.id);
|
|
4169
|
+
return shouldLoad
|
|
4170
|
+
? callLoaderOrAction(
|
|
4171
|
+
type,
|
|
4172
|
+
request,
|
|
4173
|
+
match,
|
|
4174
|
+
manifest,
|
|
4175
|
+
mapRouteProperties,
|
|
4176
|
+
handlerOverride,
|
|
4177
|
+
requestContext
|
|
4178
|
+
)
|
|
4179
|
+
: Promise.resolve({ type: ResultType.data, result: undefined });
|
|
4180
|
+
};
|
|
4181
|
+
|
|
4182
|
+
return {
|
|
4183
|
+
...match,
|
|
4184
|
+
shouldLoad,
|
|
4185
|
+
resolve,
|
|
4186
|
+
};
|
|
4187
|
+
}),
|
|
4188
|
+
request,
|
|
4189
|
+
params: matches[0].params,
|
|
4190
|
+
context: requestContext,
|
|
4191
|
+
});
|
|
4192
|
+
|
|
4193
|
+
// Throw if any loadRoute implementations not called since they are what
|
|
4194
|
+
// ensures a route is fully loaded
|
|
4195
|
+
matches.forEach((m) =>
|
|
4196
|
+
invariant(
|
|
4197
|
+
loadedMatches.has(m.route.id),
|
|
4198
|
+
`\`match.resolve()\` was not called for route id "${m.route.id}". ` +
|
|
4199
|
+
"You must call `match.resolve()` on every match passed to " +
|
|
4200
|
+
"`dataStrategy` to ensure all routes are properly loaded."
|
|
4201
|
+
)
|
|
4202
|
+
);
|
|
4203
|
+
|
|
4204
|
+
// Filter out any middleware-only matches for which we didn't need to run handlers
|
|
4205
|
+
return results.filter((_, i) => routeIdsToLoad.has(matches[i].route.id));
|
|
4206
|
+
}
|
|
4207
|
+
|
|
4208
|
+
// Default logic for calling a loader/action is the user has no specified a dataStrategy
|
|
3957
4209
|
async function callLoaderOrAction(
|
|
3958
4210
|
type: "loader" | "action",
|
|
3959
4211
|
request: Request,
|
|
3960
4212
|
match: AgnosticDataRouteMatch,
|
|
3961
|
-
matches: AgnosticDataRouteMatch[],
|
|
3962
4213
|
manifest: RouteManifest,
|
|
3963
4214
|
mapRouteProperties: MapRoutePropertiesFunction,
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
isRouteRequest?: boolean;
|
|
3969
|
-
requestContext?: unknown;
|
|
3970
|
-
} = {}
|
|
3971
|
-
): Promise<DataResult> {
|
|
3972
|
-
let resultType;
|
|
3973
|
-
let result;
|
|
4215
|
+
handlerOverride: Parameters<DataStrategyMatch["resolve"]>[0],
|
|
4216
|
+
staticContext?: unknown
|
|
4217
|
+
): Promise<HandlerResult> {
|
|
4218
|
+
let result: HandlerResult;
|
|
3974
4219
|
let onReject: (() => void) | undefined;
|
|
3975
4220
|
|
|
3976
|
-
let runHandler = (
|
|
4221
|
+
let runHandler = (
|
|
4222
|
+
handler: AgnosticRouteObject["loader"] | AgnosticRouteObject["action"]
|
|
4223
|
+
): Promise<HandlerResult> => {
|
|
3977
4224
|
// Setup a promise we can race against so that abort signals short circuit
|
|
3978
4225
|
let reject: () => void;
|
|
3979
|
-
|
|
4226
|
+
// This will never resolve so safe to type it as Promise<HandlerResult> to
|
|
4227
|
+
// satisfy the function return value
|
|
4228
|
+
let abortPromise = new Promise<HandlerResult>((_, r) => (reject = r));
|
|
3980
4229
|
onReject = () => reject();
|
|
3981
4230
|
request.signal.addEventListener("abort", onReject);
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
4231
|
+
|
|
4232
|
+
let actualHandler = (ctx?: unknown) => {
|
|
4233
|
+
if (typeof handler !== "function") {
|
|
4234
|
+
return Promise.reject(
|
|
4235
|
+
new Error(
|
|
4236
|
+
`You cannot call the handler for a route which defines a boolean ` +
|
|
4237
|
+
`"${type}" [routeId: ${match.route.id}]`
|
|
4238
|
+
)
|
|
4239
|
+
);
|
|
4240
|
+
}
|
|
4241
|
+
return handler(
|
|
4242
|
+
{
|
|
4243
|
+
request,
|
|
4244
|
+
params: match.params,
|
|
4245
|
+
context: staticContext,
|
|
4246
|
+
},
|
|
4247
|
+
...(ctx !== undefined ? [ctx] : [])
|
|
4248
|
+
);
|
|
4249
|
+
};
|
|
4250
|
+
|
|
4251
|
+
let handlerPromise: Promise<HandlerResult>;
|
|
4252
|
+
if (handlerOverride) {
|
|
4253
|
+
handlerPromise = handlerOverride((ctx: unknown) => actualHandler(ctx));
|
|
4254
|
+
} else {
|
|
4255
|
+
handlerPromise = (async () => {
|
|
4256
|
+
try {
|
|
4257
|
+
let val = await actualHandler();
|
|
4258
|
+
return { type: "data", result: val };
|
|
4259
|
+
} catch (e) {
|
|
4260
|
+
return { type: "error", result: e };
|
|
4261
|
+
}
|
|
4262
|
+
})();
|
|
4263
|
+
}
|
|
4264
|
+
|
|
4265
|
+
return Promise.race([handlerPromise, abortPromise]);
|
|
3990
4266
|
};
|
|
3991
4267
|
|
|
3992
4268
|
try {
|
|
@@ -3996,7 +4272,7 @@ async function callLoaderOrAction(
|
|
|
3996
4272
|
if (handler) {
|
|
3997
4273
|
// Run statically defined handler in parallel with lazy()
|
|
3998
4274
|
let handlerError;
|
|
3999
|
-
let
|
|
4275
|
+
let [value] = await Promise.all([
|
|
4000
4276
|
// If the handler throws, don't let it immediately bubble out,
|
|
4001
4277
|
// since we need to let the lazy() execution finish so we know if this
|
|
4002
4278
|
// route has a boundary that can handle the error
|
|
@@ -4005,17 +4281,17 @@ async function callLoaderOrAction(
|
|
|
4005
4281
|
}),
|
|
4006
4282
|
loadLazyRouteModule(match.route, mapRouteProperties, manifest),
|
|
4007
4283
|
]);
|
|
4008
|
-
if (handlerError) {
|
|
4284
|
+
if (handlerError !== undefined) {
|
|
4009
4285
|
throw handlerError;
|
|
4010
4286
|
}
|
|
4011
|
-
result =
|
|
4287
|
+
result = value!;
|
|
4012
4288
|
} else {
|
|
4013
4289
|
// Load lazy route module, then run any returned handler
|
|
4014
4290
|
await loadLazyRouteModule(match.route, mapRouteProperties, manifest);
|
|
4015
4291
|
|
|
4016
4292
|
handler = match.route[type];
|
|
4017
4293
|
if (handler) {
|
|
4018
|
-
// Handler still
|
|
4294
|
+
// Handler still runs even if we got interrupted to maintain consistency
|
|
4019
4295
|
// with un-abortable behavior of handler execution on non-lazy or
|
|
4020
4296
|
// previously-lazy-loaded routes
|
|
4021
4297
|
result = await runHandler(handler);
|
|
@@ -4030,7 +4306,7 @@ async function callLoaderOrAction(
|
|
|
4030
4306
|
} else {
|
|
4031
4307
|
// lazy() route has no loader to run. Short circuit here so we don't
|
|
4032
4308
|
// hit the invariant below that errors on returning undefined.
|
|
4033
|
-
return { type: ResultType.data,
|
|
4309
|
+
return { type: ResultType.data, result: undefined };
|
|
4034
4310
|
}
|
|
4035
4311
|
}
|
|
4036
4312
|
} else if (!handler) {
|
|
@@ -4044,85 +4320,31 @@ async function callLoaderOrAction(
|
|
|
4044
4320
|
}
|
|
4045
4321
|
|
|
4046
4322
|
invariant(
|
|
4047
|
-
result !== undefined,
|
|
4323
|
+
result.result !== undefined,
|
|
4048
4324
|
`You defined ${type === "action" ? "an action" : "a loader"} for route ` +
|
|
4049
4325
|
`"${match.route.id}" but didn't return anything from your \`${type}\` ` +
|
|
4050
4326
|
`function. Please return a value or \`null\`.`
|
|
4051
4327
|
);
|
|
4052
4328
|
} catch (e) {
|
|
4053
|
-
|
|
4054
|
-
|
|
4329
|
+
// We should already be catching and converting normal handler executions to
|
|
4330
|
+
// HandlerResults and returning them, so anything that throws here is an
|
|
4331
|
+
// unexpected error we still need to wrap
|
|
4332
|
+
return { type: ResultType.error, result: e };
|
|
4055
4333
|
} finally {
|
|
4056
4334
|
if (onReject) {
|
|
4057
4335
|
request.signal.removeEventListener("abort", onReject);
|
|
4058
4336
|
}
|
|
4059
4337
|
}
|
|
4060
4338
|
|
|
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
|
-
}
|
|
4339
|
+
return result;
|
|
4340
|
+
}
|
|
4113
4341
|
|
|
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
|
-
}
|
|
4342
|
+
async function convertHandlerResultToDataResult(
|
|
4343
|
+
handlerResult: HandlerResult
|
|
4344
|
+
): Promise<DataResult> {
|
|
4345
|
+
let { result, type, status } = handlerResult;
|
|
4125
4346
|
|
|
4347
|
+
if (isResponse(result)) {
|
|
4126
4348
|
let data: any;
|
|
4127
4349
|
|
|
4128
4350
|
try {
|
|
@@ -4142,10 +4364,11 @@ async function callLoaderOrAction(
|
|
|
4142
4364
|
return { type: ResultType.error, error: e };
|
|
4143
4365
|
}
|
|
4144
4366
|
|
|
4145
|
-
if (
|
|
4367
|
+
if (type === ResultType.error) {
|
|
4146
4368
|
return {
|
|
4147
|
-
type:
|
|
4148
|
-
error: new ErrorResponseImpl(status, result.statusText, data),
|
|
4369
|
+
type: ResultType.error,
|
|
4370
|
+
error: new ErrorResponseImpl(result.status, result.statusText, data),
|
|
4371
|
+
statusCode: result.status,
|
|
4149
4372
|
headers: result.headers,
|
|
4150
4373
|
};
|
|
4151
4374
|
}
|
|
@@ -4158,8 +4381,12 @@ async function callLoaderOrAction(
|
|
|
4158
4381
|
};
|
|
4159
4382
|
}
|
|
4160
4383
|
|
|
4161
|
-
if (
|
|
4162
|
-
return {
|
|
4384
|
+
if (type === ResultType.error) {
|
|
4385
|
+
return {
|
|
4386
|
+
type: ResultType.error,
|
|
4387
|
+
error: result,
|
|
4388
|
+
statusCode: isRouteErrorResponse(result) ? result.status : status,
|
|
4389
|
+
};
|
|
4163
4390
|
}
|
|
4164
4391
|
|
|
4165
4392
|
if (isDeferredData(result)) {
|
|
@@ -4171,7 +4398,60 @@ async function callLoaderOrAction(
|
|
|
4171
4398
|
};
|
|
4172
4399
|
}
|
|
4173
4400
|
|
|
4174
|
-
return { type: ResultType.data, data: result };
|
|
4401
|
+
return { type: ResultType.data, data: result, statusCode: status };
|
|
4402
|
+
}
|
|
4403
|
+
|
|
4404
|
+
// Support relative routing in internal redirects
|
|
4405
|
+
function normalizeRelativeRoutingRedirectResponse(
|
|
4406
|
+
response: Response,
|
|
4407
|
+
request: Request,
|
|
4408
|
+
routeId: string,
|
|
4409
|
+
matches: AgnosticDataRouteMatch[],
|
|
4410
|
+
basename: string,
|
|
4411
|
+
v7_relativeSplatPath: boolean
|
|
4412
|
+
) {
|
|
4413
|
+
let location = response.headers.get("Location");
|
|
4414
|
+
invariant(
|
|
4415
|
+
location,
|
|
4416
|
+
"Redirects returned/thrown from loaders/actions must have a Location header"
|
|
4417
|
+
);
|
|
4418
|
+
|
|
4419
|
+
if (!ABSOLUTE_URL_REGEX.test(location)) {
|
|
4420
|
+
let trimmedMatches = matches.slice(
|
|
4421
|
+
0,
|
|
4422
|
+
matches.findIndex((m) => m.route.id === routeId) + 1
|
|
4423
|
+
);
|
|
4424
|
+
location = normalizeTo(
|
|
4425
|
+
new URL(request.url),
|
|
4426
|
+
trimmedMatches,
|
|
4427
|
+
basename,
|
|
4428
|
+
true,
|
|
4429
|
+
location,
|
|
4430
|
+
v7_relativeSplatPath
|
|
4431
|
+
);
|
|
4432
|
+
response.headers.set("Location", location);
|
|
4433
|
+
}
|
|
4434
|
+
|
|
4435
|
+
return response;
|
|
4436
|
+
}
|
|
4437
|
+
|
|
4438
|
+
function normalizeRedirectLocation(
|
|
4439
|
+
location: string,
|
|
4440
|
+
currentUrl: URL,
|
|
4441
|
+
basename: string
|
|
4442
|
+
): string {
|
|
4443
|
+
if (ABSOLUTE_URL_REGEX.test(location)) {
|
|
4444
|
+
// Strip off the protocol+origin for same-origin + same-basename absolute redirects
|
|
4445
|
+
let normalizedLocation = location;
|
|
4446
|
+
let url = normalizedLocation.startsWith("//")
|
|
4447
|
+
? new URL(currentUrl.protocol + normalizedLocation)
|
|
4448
|
+
: new URL(normalizedLocation);
|
|
4449
|
+
let isSameBasename = stripBasename(url.pathname, basename) != null;
|
|
4450
|
+
if (url.origin === currentUrl.origin && isSameBasename) {
|
|
4451
|
+
return url.pathname + url.search + url.hash;
|
|
4452
|
+
}
|
|
4453
|
+
}
|
|
4454
|
+
return location;
|
|
4175
4455
|
}
|
|
4176
4456
|
|
|
4177
4457
|
// Utility method for creating the Request instances for loaders/actions during
|
|
@@ -4239,8 +4519,9 @@ function processRouteLoaderData(
|
|
|
4239
4519
|
matches: AgnosticDataRouteMatch[],
|
|
4240
4520
|
matchesToLoad: AgnosticDataRouteMatch[],
|
|
4241
4521
|
results: DataResult[],
|
|
4242
|
-
|
|
4243
|
-
activeDeferreds: Map<string, DeferredData
|
|
4522
|
+
pendingActionResult: PendingActionResult | undefined,
|
|
4523
|
+
activeDeferreds: Map<string, DeferredData>,
|
|
4524
|
+
skipLoaderErrorBubbling: boolean
|
|
4244
4525
|
): {
|
|
4245
4526
|
loaderData: RouterState["loaderData"];
|
|
4246
4527
|
errors: RouterState["errors"] | null;
|
|
@@ -4253,6 +4534,10 @@ function processRouteLoaderData(
|
|
|
4253
4534
|
let statusCode: number | undefined;
|
|
4254
4535
|
let foundError = false;
|
|
4255
4536
|
let loaderHeaders: Record<string, Headers> = {};
|
|
4537
|
+
let pendingError =
|
|
4538
|
+
pendingActionResult && isErrorResult(pendingActionResult[1])
|
|
4539
|
+
? pendingActionResult[1].error
|
|
4540
|
+
: undefined;
|
|
4256
4541
|
|
|
4257
4542
|
// Process loader results into state.loaderData/state.errors
|
|
4258
4543
|
results.forEach((result, index) => {
|
|
@@ -4262,23 +4547,27 @@ function processRouteLoaderData(
|
|
|
4262
4547
|
"Cannot handle redirect results in processLoaderData"
|
|
4263
4548
|
);
|
|
4264
4549
|
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
4550
|
let error = result.error;
|
|
4269
4551
|
// If we have a pending action error, we report it at the highest-route
|
|
4270
4552
|
// that throws a loader error, and then clear it out to indicate that
|
|
4271
4553
|
// it was consumed
|
|
4272
|
-
if (pendingError) {
|
|
4273
|
-
error =
|
|
4554
|
+
if (pendingError !== undefined) {
|
|
4555
|
+
error = pendingError;
|
|
4274
4556
|
pendingError = undefined;
|
|
4275
4557
|
}
|
|
4276
4558
|
|
|
4277
4559
|
errors = errors || {};
|
|
4278
4560
|
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4561
|
+
if (skipLoaderErrorBubbling) {
|
|
4562
|
+
errors[id] = error;
|
|
4563
|
+
} else {
|
|
4564
|
+
// Look upwards from the matched route for the closest ancestor error
|
|
4565
|
+
// boundary, defaulting to the root match. Prefer higher error values
|
|
4566
|
+
// if lower errors bubble to the same boundary
|
|
4567
|
+
let boundaryMatch = findNearestBoundary(matches, id);
|
|
4568
|
+
if (errors[boundaryMatch.route.id] == null) {
|
|
4569
|
+
errors[boundaryMatch.route.id] = error;
|
|
4570
|
+
}
|
|
4282
4571
|
}
|
|
4283
4572
|
|
|
4284
4573
|
// Clear our any prior loaderData for the throwing route
|
|
@@ -4299,21 +4588,28 @@ function processRouteLoaderData(
|
|
|
4299
4588
|
if (isDeferredResult(result)) {
|
|
4300
4589
|
activeDeferreds.set(id, result.deferredData);
|
|
4301
4590
|
loaderData[id] = result.deferredData.data;
|
|
4591
|
+
// Error status codes always override success status codes, but if all
|
|
4592
|
+
// loaders are successful we take the deepest status code.
|
|
4593
|
+
if (
|
|
4594
|
+
result.statusCode != null &&
|
|
4595
|
+
result.statusCode !== 200 &&
|
|
4596
|
+
!foundError
|
|
4597
|
+
) {
|
|
4598
|
+
statusCode = result.statusCode;
|
|
4599
|
+
}
|
|
4600
|
+
if (result.headers) {
|
|
4601
|
+
loaderHeaders[id] = result.headers;
|
|
4602
|
+
}
|
|
4302
4603
|
} else {
|
|
4303
4604
|
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;
|
|
4605
|
+
// Error status codes always override success status codes, but if all
|
|
4606
|
+
// loaders are successful we take the deepest status code.
|
|
4607
|
+
if (result.statusCode && result.statusCode !== 200 && !foundError) {
|
|
4608
|
+
statusCode = result.statusCode;
|
|
4609
|
+
}
|
|
4610
|
+
if (result.headers) {
|
|
4611
|
+
loaderHeaders[id] = result.headers;
|
|
4612
|
+
}
|
|
4317
4613
|
}
|
|
4318
4614
|
}
|
|
4319
4615
|
});
|
|
@@ -4321,9 +4617,9 @@ function processRouteLoaderData(
|
|
|
4321
4617
|
// If we didn't consume the pending action error (i.e., all loaders
|
|
4322
4618
|
// resolved), then consume it here. Also clear out any loaderData for the
|
|
4323
4619
|
// throwing route
|
|
4324
|
-
if (pendingError) {
|
|
4325
|
-
errors = pendingError;
|
|
4326
|
-
loaderData[
|
|
4620
|
+
if (pendingError !== undefined && pendingActionResult) {
|
|
4621
|
+
errors = { [pendingActionResult[0]]: pendingError };
|
|
4622
|
+
loaderData[pendingActionResult[0]] = undefined;
|
|
4327
4623
|
}
|
|
4328
4624
|
|
|
4329
4625
|
return {
|
|
@@ -4339,7 +4635,7 @@ function processLoaderData(
|
|
|
4339
4635
|
matches: AgnosticDataRouteMatch[],
|
|
4340
4636
|
matchesToLoad: AgnosticDataRouteMatch[],
|
|
4341
4637
|
results: DataResult[],
|
|
4342
|
-
|
|
4638
|
+
pendingActionResult: PendingActionResult | undefined,
|
|
4343
4639
|
revalidatingFetchers: RevalidatingFetcher[],
|
|
4344
4640
|
fetcherResults: DataResult[],
|
|
4345
4641
|
activeDeferreds: Map<string, DeferredData>
|
|
@@ -4351,8 +4647,9 @@ function processLoaderData(
|
|
|
4351
4647
|
matches,
|
|
4352
4648
|
matchesToLoad,
|
|
4353
4649
|
results,
|
|
4354
|
-
|
|
4355
|
-
activeDeferreds
|
|
4650
|
+
pendingActionResult,
|
|
4651
|
+
activeDeferreds,
|
|
4652
|
+
false // This method is only called client side so we always want to bubble
|
|
4356
4653
|
);
|
|
4357
4654
|
|
|
4358
4655
|
// Process results from our revalidating fetchers
|
|
@@ -4425,6 +4722,24 @@ function mergeLoaderData(
|
|
|
4425
4722
|
return mergedLoaderData;
|
|
4426
4723
|
}
|
|
4427
4724
|
|
|
4725
|
+
function getActionDataForCommit(
|
|
4726
|
+
pendingActionResult: PendingActionResult | undefined
|
|
4727
|
+
) {
|
|
4728
|
+
if (!pendingActionResult) {
|
|
4729
|
+
return {};
|
|
4730
|
+
}
|
|
4731
|
+
return isErrorResult(pendingActionResult[1])
|
|
4732
|
+
? {
|
|
4733
|
+
// Clear out prior actionData on errors
|
|
4734
|
+
actionData: {},
|
|
4735
|
+
}
|
|
4736
|
+
: {
|
|
4737
|
+
actionData: {
|
|
4738
|
+
[pendingActionResult[0]]: pendingActionResult[1].data,
|
|
4739
|
+
},
|
|
4740
|
+
};
|
|
4741
|
+
}
|
|
4742
|
+
|
|
4428
4743
|
// Find the nearest error boundary, looking upwards from the leaf route (or the
|
|
4429
4744
|
// route specified by routeId) for the closest ancestor error boundary,
|
|
4430
4745
|
// defaulting to the root match
|
|
@@ -4559,6 +4874,22 @@ function isHashChangeOnly(a: Location, b: Location): boolean {
|
|
|
4559
4874
|
return false;
|
|
4560
4875
|
}
|
|
4561
4876
|
|
|
4877
|
+
function isHandlerResult(result: unknown): result is HandlerResult {
|
|
4878
|
+
return (
|
|
4879
|
+
result != null &&
|
|
4880
|
+
typeof result === "object" &&
|
|
4881
|
+
"type" in result &&
|
|
4882
|
+
"result" in result &&
|
|
4883
|
+
(result.type === ResultType.data || result.type === ResultType.error)
|
|
4884
|
+
);
|
|
4885
|
+
}
|
|
4886
|
+
|
|
4887
|
+
function isRedirectHandlerResult(result: HandlerResult) {
|
|
4888
|
+
return (
|
|
4889
|
+
isResponse(result.result) && redirectStatusCodes.has(result.result.status)
|
|
4890
|
+
);
|
|
4891
|
+
}
|
|
4892
|
+
|
|
4562
4893
|
function isDeferredResult(result: DataResult): result is DeferredResult {
|
|
4563
4894
|
return result.type === ResultType.deferred;
|
|
4564
4895
|
}
|
|
@@ -4603,14 +4934,6 @@ function isRedirectResponse(result: any): result is Response {
|
|
|
4603
4934
|
return status >= 300 && status <= 399 && location != null;
|
|
4604
4935
|
}
|
|
4605
4936
|
|
|
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
4937
|
function isValidMethod(method: string): method is FormMethod | V7_FormMethod {
|
|
4615
4938
|
return validRequestMethods.has(method.toLowerCase() as FormMethod);
|
|
4616
4939
|
}
|