@remix-run/router 1.0.3 → 1.0.4-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 +10 -0
- package/dist/history.d.ts +3 -3
- package/dist/router.cjs.js +257 -166
- package/dist/router.cjs.js.map +1 -1
- package/dist/router.d.ts +14 -2
- package/dist/router.js +257 -166
- package/dist/router.js.map +1 -1
- package/dist/router.umd.js +257 -166
- 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 +7 -3
- package/history.ts +12 -8
- package/package.json +1 -1
- package/router.ts +288 -183
- package/utils.ts +21 -5
package/router.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { History, Location, To } from "./history";
|
|
1
|
+
import type { History, Location, Path, To } from "./history";
|
|
2
2
|
import {
|
|
3
3
|
Action as HistoryAction,
|
|
4
4
|
createLocation,
|
|
@@ -20,6 +20,7 @@ import type {
|
|
|
20
20
|
Submission,
|
|
21
21
|
SuccessResult,
|
|
22
22
|
AgnosticRouteMatch,
|
|
23
|
+
SubmissionFormMethod,
|
|
23
24
|
} from "./utils";
|
|
24
25
|
import {
|
|
25
26
|
DeferredData,
|
|
@@ -154,6 +155,16 @@ export interface Router {
|
|
|
154
155
|
*/
|
|
155
156
|
createHref(location: Location | URL): string;
|
|
156
157
|
|
|
158
|
+
/**
|
|
159
|
+
* @internal
|
|
160
|
+
* PRIVATE - DO NOT USE
|
|
161
|
+
*
|
|
162
|
+
* Utility function to URL encode a destination path according to the internal
|
|
163
|
+
* history implementation
|
|
164
|
+
* @param to
|
|
165
|
+
*/
|
|
166
|
+
encodeLocation(to: To): Path;
|
|
167
|
+
|
|
157
168
|
/**
|
|
158
169
|
* @internal
|
|
159
170
|
* PRIVATE - DO NOT USE
|
|
@@ -288,6 +299,7 @@ export interface RouterInit {
|
|
|
288
299
|
* State returned from a server-side query() call
|
|
289
300
|
*/
|
|
290
301
|
export interface StaticHandlerContext {
|
|
302
|
+
basename: Router["basename"];
|
|
291
303
|
location: RouterState["location"];
|
|
292
304
|
matches: RouterState["matches"];
|
|
293
305
|
loaderData: RouterState["loaderData"];
|
|
@@ -503,6 +515,20 @@ interface QueryRouteResponse {
|
|
|
503
515
|
response: Response;
|
|
504
516
|
}
|
|
505
517
|
|
|
518
|
+
const validActionMethodsArr: SubmissionFormMethod[] = [
|
|
519
|
+
"post",
|
|
520
|
+
"put",
|
|
521
|
+
"patch",
|
|
522
|
+
"delete",
|
|
523
|
+
];
|
|
524
|
+
const validActionMethods = new Set<SubmissionFormMethod>(validActionMethodsArr);
|
|
525
|
+
|
|
526
|
+
const validRequestMethodsArr: FormMethod[] = ["get", ...validActionMethodsArr];
|
|
527
|
+
const validRequestMethods = new Set<FormMethod>(validRequestMethodsArr);
|
|
528
|
+
|
|
529
|
+
const redirectStatusCodes = new Set([301, 302, 303, 307, 308]);
|
|
530
|
+
const redirectPreserveMethodStatusCodes = new Set([307, 308]);
|
|
531
|
+
|
|
506
532
|
export const IDLE_NAVIGATION: NavigationStates["Idle"] = {
|
|
507
533
|
state: "idle",
|
|
508
534
|
location: undefined,
|
|
@@ -568,7 +594,10 @@ export function createRouter(init: RouterInit): Router {
|
|
|
568
594
|
if (initialMatches == null) {
|
|
569
595
|
// If we do not match a user-provided-route, fall back to the root
|
|
570
596
|
// to allow the error boundary to take over
|
|
571
|
-
let
|
|
597
|
+
let error = getInternalRouterError(404, {
|
|
598
|
+
pathname: init.history.location.pathname,
|
|
599
|
+
});
|
|
600
|
+
let { matches, route } = getShortCircuitMatches(dataRoutes);
|
|
572
601
|
initialMatches = matches;
|
|
573
602
|
initialErrors = { [route.id]: error };
|
|
574
603
|
}
|
|
@@ -770,7 +799,10 @@ export function createRouter(init: RouterInit): Router {
|
|
|
770
799
|
// remains the same as POP and non-data-router usages. new URL() does all
|
|
771
800
|
// the same encoding we'd get from a history.pushState/window.location read
|
|
772
801
|
// without having to touch history
|
|
773
|
-
location =
|
|
802
|
+
location = {
|
|
803
|
+
...location,
|
|
804
|
+
...init.history.encodeLocation(location),
|
|
805
|
+
};
|
|
774
806
|
|
|
775
807
|
let historyAction =
|
|
776
808
|
(opts && opts.replace) === true || submission != null
|
|
@@ -858,11 +890,9 @@ export function createRouter(init: RouterInit): Router {
|
|
|
858
890
|
|
|
859
891
|
// Short circuit with a 404 on the root error boundary if we match nothing
|
|
860
892
|
if (!matches) {
|
|
861
|
-
let {
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
error,
|
|
865
|
-
} = getNotFoundMatches(dataRoutes);
|
|
893
|
+
let error = getInternalRouterError(404, { pathname: location.pathname });
|
|
894
|
+
let { matches: notFoundMatches, route } =
|
|
895
|
+
getShortCircuitMatches(dataRoutes);
|
|
866
896
|
// Cancel all pending deferred on 404s since we don't keep any routes
|
|
867
897
|
cancelActiveDeferreds();
|
|
868
898
|
completeNavigation(location, {
|
|
@@ -976,7 +1006,14 @@ export function createRouter(init: RouterInit): Router {
|
|
|
976
1006
|
let actionMatch = getTargetMatch(matches, location);
|
|
977
1007
|
|
|
978
1008
|
if (!actionMatch.route.action) {
|
|
979
|
-
result =
|
|
1009
|
+
result = {
|
|
1010
|
+
type: ResultType.error,
|
|
1011
|
+
error: getInternalRouterError(405, {
|
|
1012
|
+
method: request.method,
|
|
1013
|
+
pathname: location.pathname,
|
|
1014
|
+
routeId: actionMatch.route.id,
|
|
1015
|
+
}),
|
|
1016
|
+
};
|
|
980
1017
|
} else {
|
|
981
1018
|
result = await callLoaderOrAction(
|
|
982
1019
|
"action",
|
|
@@ -992,15 +1029,10 @@ export function createRouter(init: RouterInit): Router {
|
|
|
992
1029
|
}
|
|
993
1030
|
|
|
994
1031
|
if (isRedirectResult(result)) {
|
|
995
|
-
let redirectNavigation: NavigationStates["Loading"] = {
|
|
996
|
-
state: "loading",
|
|
997
|
-
location: createLocation(state.location, result.location),
|
|
998
|
-
...submission,
|
|
999
|
-
};
|
|
1000
1032
|
await startRedirectNavigation(
|
|
1033
|
+
state,
|
|
1001
1034
|
result,
|
|
1002
|
-
|
|
1003
|
-
opts && opts.replace
|
|
1035
|
+
opts && opts.replace === true
|
|
1004
1036
|
);
|
|
1005
1037
|
return { shortCircuited: true };
|
|
1006
1038
|
}
|
|
@@ -1144,8 +1176,7 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1144
1176
|
// If any loaders returned a redirect Response, start a new REPLACE navigation
|
|
1145
1177
|
let redirect = findRedirect(results);
|
|
1146
1178
|
if (redirect) {
|
|
1147
|
-
|
|
1148
|
-
await startRedirectNavigation(redirect, redirectNavigation, replace);
|
|
1179
|
+
await startRedirectNavigation(state, redirect, replace);
|
|
1149
1180
|
return { shortCircuited: true };
|
|
1150
1181
|
}
|
|
1151
1182
|
|
|
@@ -1208,7 +1239,11 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1208
1239
|
|
|
1209
1240
|
let matches = matchRoutes(dataRoutes, href, init.basename);
|
|
1210
1241
|
if (!matches) {
|
|
1211
|
-
setFetcherError(
|
|
1242
|
+
setFetcherError(
|
|
1243
|
+
key,
|
|
1244
|
+
routeId,
|
|
1245
|
+
getInternalRouterError(404, { pathname: href })
|
|
1246
|
+
);
|
|
1212
1247
|
return;
|
|
1213
1248
|
}
|
|
1214
1249
|
|
|
@@ -1240,7 +1275,11 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1240
1275
|
fetchLoadMatches.delete(key);
|
|
1241
1276
|
|
|
1242
1277
|
if (!match.route.action) {
|
|
1243
|
-
let
|
|
1278
|
+
let error = getInternalRouterError(405, {
|
|
1279
|
+
method: submission.formMethod,
|
|
1280
|
+
pathname: path,
|
|
1281
|
+
routeId: routeId,
|
|
1282
|
+
});
|
|
1244
1283
|
setFetcherError(key, routeId, error);
|
|
1245
1284
|
return;
|
|
1246
1285
|
}
|
|
@@ -1288,13 +1327,7 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1288
1327
|
state.fetchers.set(key, loadingFetcher);
|
|
1289
1328
|
updateState({ fetchers: new Map(state.fetchers) });
|
|
1290
1329
|
|
|
1291
|
-
|
|
1292
|
-
state: "loading",
|
|
1293
|
-
location: createLocation(state.location, actionResult.location),
|
|
1294
|
-
...submission,
|
|
1295
|
-
};
|
|
1296
|
-
await startRedirectNavigation(actionResult, redirectNavigation);
|
|
1297
|
-
return;
|
|
1330
|
+
return startRedirectNavigation(state, actionResult);
|
|
1298
1331
|
}
|
|
1299
1332
|
|
|
1300
1333
|
// Process any non-redirect errors thrown
|
|
@@ -1386,9 +1419,7 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1386
1419
|
|
|
1387
1420
|
let redirect = findRedirect(results);
|
|
1388
1421
|
if (redirect) {
|
|
1389
|
-
|
|
1390
|
-
await startRedirectNavigation(redirect, redirectNavigation);
|
|
1391
|
-
return;
|
|
1422
|
+
return startRedirectNavigation(state, redirect);
|
|
1392
1423
|
}
|
|
1393
1424
|
|
|
1394
1425
|
// Process and commit output from loaders
|
|
@@ -1499,8 +1530,7 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1499
1530
|
|
|
1500
1531
|
// If the loader threw a redirect Response, start a new REPLACE navigation
|
|
1501
1532
|
if (isRedirectResult(result)) {
|
|
1502
|
-
|
|
1503
|
-
await startRedirectNavigation(result, redirectNavigation);
|
|
1533
|
+
await startRedirectNavigation(state, result);
|
|
1504
1534
|
return;
|
|
1505
1535
|
}
|
|
1506
1536
|
|
|
@@ -1555,17 +1585,29 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1555
1585
|
* the history action from the original navigation (PUSH or REPLACE).
|
|
1556
1586
|
*/
|
|
1557
1587
|
async function startRedirectNavigation(
|
|
1588
|
+
state: RouterState,
|
|
1558
1589
|
redirect: RedirectResult,
|
|
1559
|
-
navigation: Navigation,
|
|
1560
1590
|
replace?: boolean
|
|
1561
1591
|
) {
|
|
1562
1592
|
if (redirect.revalidate) {
|
|
1563
1593
|
isRevalidationRequired = true;
|
|
1564
1594
|
}
|
|
1595
|
+
|
|
1596
|
+
let redirectLocation = createLocation(state.location, redirect.location);
|
|
1565
1597
|
invariant(
|
|
1566
|
-
|
|
1598
|
+
redirectLocation,
|
|
1567
1599
|
"Expected a location on the redirect navigation"
|
|
1568
1600
|
);
|
|
1601
|
+
|
|
1602
|
+
if (
|
|
1603
|
+
redirect.external &&
|
|
1604
|
+
typeof window !== "undefined" &&
|
|
1605
|
+
typeof window.location !== "undefined"
|
|
1606
|
+
) {
|
|
1607
|
+
window.location.replace(redirect.location);
|
|
1608
|
+
return;
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1569
1611
|
// There's no need to abort on redirects, since we don't detect the
|
|
1570
1612
|
// redirect until the action/loaders have settled
|
|
1571
1613
|
pendingNavigationController = null;
|
|
@@ -1573,9 +1615,40 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1573
1615
|
let redirectHistoryAction =
|
|
1574
1616
|
replace === true ? HistoryAction.Replace : HistoryAction.Push;
|
|
1575
1617
|
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1618
|
+
let { formMethod, formAction, formEncType, formData } = state.navigation;
|
|
1619
|
+
|
|
1620
|
+
// If this was a 307/308 submission we want to preserve the HTTP method and
|
|
1621
|
+
// re-submit the POST/PUT/PATCH/DELETE as a submission navigation to the
|
|
1622
|
+
// redirected location
|
|
1623
|
+
if (
|
|
1624
|
+
redirectPreserveMethodStatusCodes.has(redirect.status) &&
|
|
1625
|
+
formMethod &&
|
|
1626
|
+
isSubmissionMethod(formMethod) &&
|
|
1627
|
+
formEncType &&
|
|
1628
|
+
formData
|
|
1629
|
+
) {
|
|
1630
|
+
await startNavigation(redirectHistoryAction, redirectLocation, {
|
|
1631
|
+
submission: {
|
|
1632
|
+
formMethod,
|
|
1633
|
+
formAction: redirect.location,
|
|
1634
|
+
formEncType,
|
|
1635
|
+
formData,
|
|
1636
|
+
},
|
|
1637
|
+
});
|
|
1638
|
+
} else {
|
|
1639
|
+
// Otherwise, we kick off a new loading navigation, preserving the
|
|
1640
|
+
// submission info for the duration of this navigation
|
|
1641
|
+
await startNavigation(redirectHistoryAction, redirectLocation, {
|
|
1642
|
+
overrideNavigation: {
|
|
1643
|
+
state: "loading",
|
|
1644
|
+
location: redirectLocation,
|
|
1645
|
+
formMethod: formMethod || undefined,
|
|
1646
|
+
formAction: formAction || undefined,
|
|
1647
|
+
formEncType: formEncType || undefined,
|
|
1648
|
+
formData: formData || undefined,
|
|
1649
|
+
},
|
|
1650
|
+
});
|
|
1651
|
+
}
|
|
1579
1652
|
}
|
|
1580
1653
|
|
|
1581
1654
|
async function callLoadersAndMaybeResolveData(
|
|
@@ -1809,6 +1882,7 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1809
1882
|
// Passthrough to history-aware createHref used by useHref so we get proper
|
|
1810
1883
|
// hash-aware URLs in DOM paths
|
|
1811
1884
|
createHref: (to: To) => init.history.createHref(to),
|
|
1885
|
+
encodeLocation: (to: To) => init.history.encodeLocation(to),
|
|
1812
1886
|
getFetcher,
|
|
1813
1887
|
deleteFetcher,
|
|
1814
1888
|
dispose,
|
|
@@ -1824,11 +1898,11 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1824
1898
|
//#region createStaticHandler
|
|
1825
1899
|
////////////////////////////////////////////////////////////////////////////////
|
|
1826
1900
|
|
|
1827
|
-
const validActionMethods = new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
1828
|
-
const validRequestMethods = new Set(["GET", "HEAD", ...validActionMethods]);
|
|
1829
|
-
|
|
1830
1901
|
export function unstable_createStaticHandler(
|
|
1831
|
-
routes: AgnosticRouteObject[]
|
|
1902
|
+
routes: AgnosticRouteObject[],
|
|
1903
|
+
opts?: {
|
|
1904
|
+
basename?: string;
|
|
1905
|
+
}
|
|
1832
1906
|
): StaticHandler {
|
|
1833
1907
|
invariant(
|
|
1834
1908
|
routes.length > 0,
|
|
@@ -1836,6 +1910,7 @@ export function unstable_createStaticHandler(
|
|
|
1836
1910
|
);
|
|
1837
1911
|
|
|
1838
1912
|
let dataRoutes = convertRoutesToDataRoutes(routes);
|
|
1913
|
+
let basename = (opts ? opts.basename : null) || "/";
|
|
1839
1914
|
|
|
1840
1915
|
/**
|
|
1841
1916
|
* The query() method is intended for document requests, in which we want to
|
|
@@ -1860,16 +1935,17 @@ export function unstable_createStaticHandler(
|
|
|
1860
1935
|
request: Request
|
|
1861
1936
|
): Promise<StaticHandlerContext | Response> {
|
|
1862
1937
|
let url = new URL(request.url);
|
|
1938
|
+
let method = request.method.toLowerCase();
|
|
1863
1939
|
let location = createLocation("", createPath(url), null, "default");
|
|
1864
|
-
let matches = matchRoutes(dataRoutes, location);
|
|
1940
|
+
let matches = matchRoutes(dataRoutes, location, basename);
|
|
1865
1941
|
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
} = getMethodNotAllowedMatches(dataRoutes);
|
|
1942
|
+
// SSR supports HEAD requests while SPA doesn't
|
|
1943
|
+
if (!isValidMethod(method) && method !== "head") {
|
|
1944
|
+
let error = getInternalRouterError(405, { method });
|
|
1945
|
+
let { matches: methodNotAllowedMatches, route } =
|
|
1946
|
+
getShortCircuitMatches(dataRoutes);
|
|
1872
1947
|
return {
|
|
1948
|
+
basename,
|
|
1873
1949
|
location,
|
|
1874
1950
|
matches: methodNotAllowedMatches,
|
|
1875
1951
|
loaderData: {},
|
|
@@ -1882,12 +1958,11 @@ export function unstable_createStaticHandler(
|
|
|
1882
1958
|
actionHeaders: {},
|
|
1883
1959
|
};
|
|
1884
1960
|
} else if (!matches) {
|
|
1885
|
-
let {
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
error,
|
|
1889
|
-
} = getNotFoundMatches(dataRoutes);
|
|
1961
|
+
let error = getInternalRouterError(404, { pathname: location.pathname });
|
|
1962
|
+
let { matches: notFoundMatches, route } =
|
|
1963
|
+
getShortCircuitMatches(dataRoutes);
|
|
1890
1964
|
return {
|
|
1965
|
+
basename,
|
|
1891
1966
|
location,
|
|
1892
1967
|
matches: notFoundMatches,
|
|
1893
1968
|
loaderData: {},
|
|
@@ -1909,7 +1984,7 @@ export function unstable_createStaticHandler(
|
|
|
1909
1984
|
// When returning StaticHandlerContext, we patch back in the location here
|
|
1910
1985
|
// since we need it for React Context. But this helps keep our submit and
|
|
1911
1986
|
// loadRouteData operating on a Request instead of a Location
|
|
1912
|
-
return { location, ...result };
|
|
1987
|
+
return { location, basename, ...result };
|
|
1913
1988
|
}
|
|
1914
1989
|
|
|
1915
1990
|
/**
|
|
@@ -1925,36 +2000,38 @@ export function unstable_createStaticHandler(
|
|
|
1925
2000
|
* can do proper boundary identification in Remix where a thrown Response
|
|
1926
2001
|
* must go to the Catch Boundary but a returned Response is happy-path.
|
|
1927
2002
|
*
|
|
1928
|
-
* One thing to note is that any Router-initiated
|
|
1929
|
-
*
|
|
1930
|
-
*
|
|
2003
|
+
* One thing to note is that any Router-initiated Errors that make sense
|
|
2004
|
+
* to associate with a status code will be thrown as an ErrorResponse
|
|
2005
|
+
* instance which include the raw Error, such that the calling context can
|
|
2006
|
+
* serialize the error as they see fit while including the proper response
|
|
2007
|
+
* code. Examples here are 404 and 405 errors that occur prior to reaching
|
|
2008
|
+
* any user-defined loaders.
|
|
1931
2009
|
*/
|
|
1932
2010
|
async function queryRoute(request: Request, routeId?: string): Promise<any> {
|
|
1933
2011
|
let url = new URL(request.url);
|
|
2012
|
+
let method = request.method.toLowerCase();
|
|
1934
2013
|
let location = createLocation("", createPath(url), null, "default");
|
|
1935
|
-
let matches = matchRoutes(dataRoutes, location);
|
|
2014
|
+
let matches = matchRoutes(dataRoutes, location, basename);
|
|
1936
2015
|
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
statusText: "Method Not Allowed",
|
|
1941
|
-
});
|
|
2016
|
+
// SSR supports HEAD requests while SPA doesn't
|
|
2017
|
+
if (!isValidMethod(method) && method !== "head") {
|
|
2018
|
+
throw getInternalRouterError(405, { method });
|
|
1942
2019
|
} else if (!matches) {
|
|
1943
|
-
throw
|
|
1944
|
-
status: 404,
|
|
1945
|
-
statusText: "Not Found",
|
|
1946
|
-
});
|
|
2020
|
+
throw getInternalRouterError(404, { pathname: location.pathname });
|
|
1947
2021
|
}
|
|
1948
2022
|
|
|
1949
2023
|
let match = routeId
|
|
1950
2024
|
? matches.find((m) => m.route.id === routeId)
|
|
1951
2025
|
: getTargetMatch(matches, location);
|
|
1952
2026
|
|
|
1953
|
-
if (!match) {
|
|
1954
|
-
throw
|
|
1955
|
-
|
|
1956
|
-
|
|
2027
|
+
if (routeId && !match) {
|
|
2028
|
+
throw getInternalRouterError(403, {
|
|
2029
|
+
pathname: location.pathname,
|
|
2030
|
+
routeId,
|
|
1957
2031
|
});
|
|
2032
|
+
} else if (!match) {
|
|
2033
|
+
// This should never hit I don't think?
|
|
2034
|
+
throw getInternalRouterError(404, { pathname: location.pathname });
|
|
1958
2035
|
}
|
|
1959
2036
|
|
|
1960
2037
|
let result = await queryImpl(request, location, matches, match);
|
|
@@ -1981,14 +2058,14 @@ export function unstable_createStaticHandler(
|
|
|
1981
2058
|
location: Location,
|
|
1982
2059
|
matches: AgnosticDataRouteMatch[],
|
|
1983
2060
|
routeMatch?: AgnosticDataRouteMatch
|
|
1984
|
-
): Promise<Omit<StaticHandlerContext, "location"> | Response> {
|
|
2061
|
+
): Promise<Omit<StaticHandlerContext, "location" | "basename"> | Response> {
|
|
1985
2062
|
invariant(
|
|
1986
2063
|
request.signal,
|
|
1987
2064
|
"query()/queryRoute() requests must contain an AbortController signal"
|
|
1988
2065
|
);
|
|
1989
2066
|
|
|
1990
2067
|
try {
|
|
1991
|
-
if (
|
|
2068
|
+
if (isSubmissionMethod(request.method.toLowerCase())) {
|
|
1992
2069
|
let result = await submit(
|
|
1993
2070
|
request,
|
|
1994
2071
|
matches,
|
|
@@ -2030,23 +2107,29 @@ export function unstable_createStaticHandler(
|
|
|
2030
2107
|
matches: AgnosticDataRouteMatch[],
|
|
2031
2108
|
actionMatch: AgnosticDataRouteMatch,
|
|
2032
2109
|
isRouteRequest: boolean
|
|
2033
|
-
): Promise<Omit<StaticHandlerContext, "location"> | Response> {
|
|
2110
|
+
): Promise<Omit<StaticHandlerContext, "location" | "basename"> | Response> {
|
|
2034
2111
|
let result: DataResult;
|
|
2112
|
+
|
|
2035
2113
|
if (!actionMatch.route.action) {
|
|
2114
|
+
let error = getInternalRouterError(405, {
|
|
2115
|
+
method: request.method,
|
|
2116
|
+
pathname: createURL(request.url).pathname,
|
|
2117
|
+
routeId: actionMatch.route.id,
|
|
2118
|
+
});
|
|
2036
2119
|
if (isRouteRequest) {
|
|
2037
|
-
throw
|
|
2038
|
-
status: 405,
|
|
2039
|
-
statusText: "Method Not Allowed",
|
|
2040
|
-
});
|
|
2120
|
+
throw error;
|
|
2041
2121
|
}
|
|
2042
|
-
result =
|
|
2122
|
+
result = {
|
|
2123
|
+
type: ResultType.error,
|
|
2124
|
+
error,
|
|
2125
|
+
};
|
|
2043
2126
|
} else {
|
|
2044
2127
|
result = await callLoaderOrAction(
|
|
2045
2128
|
"action",
|
|
2046
2129
|
request,
|
|
2047
2130
|
actionMatch,
|
|
2048
2131
|
matches,
|
|
2049
|
-
|
|
2132
|
+
basename,
|
|
2050
2133
|
true,
|
|
2051
2134
|
isRouteRequest
|
|
2052
2135
|
);
|
|
@@ -2078,20 +2161,7 @@ export function unstable_createStaticHandler(
|
|
|
2078
2161
|
// Note: This should only be non-Response values if we get here, since
|
|
2079
2162
|
// isRouteRequest should throw any Response received in callLoaderOrAction
|
|
2080
2163
|
if (isErrorResult(result)) {
|
|
2081
|
-
|
|
2082
|
-
return {
|
|
2083
|
-
matches: [actionMatch],
|
|
2084
|
-
loaderData: {},
|
|
2085
|
-
actionData: null,
|
|
2086
|
-
errors: {
|
|
2087
|
-
[boundaryMatch.route.id]: result.error,
|
|
2088
|
-
},
|
|
2089
|
-
// Note: statusCode + headers are unused here since queryRoute will
|
|
2090
|
-
// return the raw Response or value
|
|
2091
|
-
statusCode: 500,
|
|
2092
|
-
loaderHeaders: {},
|
|
2093
|
-
actionHeaders: {},
|
|
2094
|
-
};
|
|
2164
|
+
throw result.error;
|
|
2095
2165
|
}
|
|
2096
2166
|
|
|
2097
2167
|
return {
|
|
@@ -2149,10 +2219,23 @@ export function unstable_createStaticHandler(
|
|
|
2149
2219
|
routeMatch?: AgnosticDataRouteMatch,
|
|
2150
2220
|
pendingActionError?: RouteData
|
|
2151
2221
|
): Promise<
|
|
2152
|
-
| Omit<
|
|
2222
|
+
| Omit<
|
|
2223
|
+
StaticHandlerContext,
|
|
2224
|
+
"location" | "basename" | "actionData" | "actionHeaders"
|
|
2225
|
+
>
|
|
2153
2226
|
| Response
|
|
2154
2227
|
> {
|
|
2155
2228
|
let isRouteRequest = routeMatch != null;
|
|
2229
|
+
|
|
2230
|
+
// Short circuit if we have no loaders to run (queryRoute())
|
|
2231
|
+
if (isRouteRequest && !routeMatch?.route.loader) {
|
|
2232
|
+
throw getInternalRouterError(400, {
|
|
2233
|
+
method: request.method,
|
|
2234
|
+
pathname: createURL(request.url).pathname,
|
|
2235
|
+
routeId: routeMatch?.route.id,
|
|
2236
|
+
});
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2156
2239
|
let requestMatches = routeMatch
|
|
2157
2240
|
? [routeMatch]
|
|
2158
2241
|
: getLoaderMatchesUntilBoundary(
|
|
@@ -2161,7 +2244,7 @@ export function unstable_createStaticHandler(
|
|
|
2161
2244
|
);
|
|
2162
2245
|
let matchesToLoad = requestMatches.filter((m) => m.route.loader);
|
|
2163
2246
|
|
|
2164
|
-
// Short circuit if we have no loaders to run
|
|
2247
|
+
// Short circuit if we have no loaders to run (query())
|
|
2165
2248
|
if (matchesToLoad.length === 0) {
|
|
2166
2249
|
return {
|
|
2167
2250
|
matches,
|
|
@@ -2179,7 +2262,7 @@ export function unstable_createStaticHandler(
|
|
|
2179
2262
|
request,
|
|
2180
2263
|
match,
|
|
2181
2264
|
matches,
|
|
2182
|
-
|
|
2265
|
+
basename,
|
|
2183
2266
|
true,
|
|
2184
2267
|
isRouteRequest
|
|
2185
2268
|
)
|
|
@@ -2213,19 +2296,6 @@ export function unstable_createStaticHandler(
|
|
|
2213
2296
|
};
|
|
2214
2297
|
}
|
|
2215
2298
|
|
|
2216
|
-
function createRouterErrorResponse(
|
|
2217
|
-
body: BodyInit | null | undefined,
|
|
2218
|
-
init: ResponseInit
|
|
2219
|
-
) {
|
|
2220
|
-
return new Response(body, {
|
|
2221
|
-
...init,
|
|
2222
|
-
headers: {
|
|
2223
|
-
...init.headers,
|
|
2224
|
-
"X-Remix-Router-Error": "yes",
|
|
2225
|
-
},
|
|
2226
|
-
});
|
|
2227
|
-
}
|
|
2228
|
-
|
|
2229
2299
|
return {
|
|
2230
2300
|
dataRoutes,
|
|
2231
2301
|
query,
|
|
@@ -2258,6 +2328,12 @@ export function getStaticContextFromError(
|
|
|
2258
2328
|
return newContext;
|
|
2259
2329
|
}
|
|
2260
2330
|
|
|
2331
|
+
function isSubmissionNavigation(
|
|
2332
|
+
opts: RouterNavigateOptions
|
|
2333
|
+
): opts is SubmissionNavigateOptions {
|
|
2334
|
+
return opts != null && "formData" in opts;
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2261
2337
|
// Normalize navigation options by converting formMethod=GET formData objects to
|
|
2262
2338
|
// URLSearchParams so they behave identically to links with query params
|
|
2263
2339
|
function normalizeNavigateOptions(
|
|
@@ -2272,12 +2348,19 @@ function normalizeNavigateOptions(
|
|
|
2272
2348
|
let path = typeof to === "string" ? to : createPath(to);
|
|
2273
2349
|
|
|
2274
2350
|
// Return location verbatim on non-submission navigations
|
|
2275
|
-
if (!opts ||
|
|
2351
|
+
if (!opts || !isSubmissionNavigation(opts)) {
|
|
2276
2352
|
return { path };
|
|
2277
2353
|
}
|
|
2278
2354
|
|
|
2355
|
+
if (opts.formMethod && !isValidMethod(opts.formMethod)) {
|
|
2356
|
+
return {
|
|
2357
|
+
path,
|
|
2358
|
+
error: getInternalRouterError(405, { method: opts.formMethod }),
|
|
2359
|
+
};
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2279
2362
|
// Create a Submission on non-GET navigations
|
|
2280
|
-
if (opts.formMethod
|
|
2363
|
+
if (opts.formMethod && isSubmissionMethod(opts.formMethod)) {
|
|
2281
2364
|
return {
|
|
2282
2365
|
path,
|
|
2283
2366
|
submission: {
|
|
@@ -2290,11 +2373,6 @@ function normalizeNavigateOptions(
|
|
|
2290
2373
|
};
|
|
2291
2374
|
}
|
|
2292
2375
|
|
|
2293
|
-
// No formData to flatten for GET submission
|
|
2294
|
-
if (!opts.formData) {
|
|
2295
|
-
return { path };
|
|
2296
|
-
}
|
|
2297
|
-
|
|
2298
2376
|
// Flatten submission onto URLSearchParams for GET submissions
|
|
2299
2377
|
let parsedPath = parsePath(path);
|
|
2300
2378
|
try {
|
|
@@ -2313,33 +2391,13 @@ function normalizeNavigateOptions(
|
|
|
2313
2391
|
} catch (e) {
|
|
2314
2392
|
return {
|
|
2315
2393
|
path,
|
|
2316
|
-
error:
|
|
2317
|
-
400,
|
|
2318
|
-
"Bad Request",
|
|
2319
|
-
"Cannot submit binary form data using GET"
|
|
2320
|
-
),
|
|
2394
|
+
error: getInternalRouterError(400),
|
|
2321
2395
|
};
|
|
2322
2396
|
}
|
|
2323
2397
|
|
|
2324
2398
|
return { path: createPath(parsedPath) };
|
|
2325
2399
|
}
|
|
2326
2400
|
|
|
2327
|
-
function getLoaderRedirect(
|
|
2328
|
-
state: RouterState,
|
|
2329
|
-
redirect: RedirectResult
|
|
2330
|
-
): Navigation {
|
|
2331
|
-
let { formMethod, formAction, formEncType, formData } = state.navigation;
|
|
2332
|
-
let navigation: NavigationStates["Loading"] = {
|
|
2333
|
-
state: "loading",
|
|
2334
|
-
location: createLocation(state.location, redirect.location),
|
|
2335
|
-
formMethod: formMethod || undefined,
|
|
2336
|
-
formAction: formAction || undefined,
|
|
2337
|
-
formEncType: formEncType || undefined,
|
|
2338
|
-
formData: formData || undefined,
|
|
2339
|
-
};
|
|
2340
|
-
return navigation;
|
|
2341
|
-
}
|
|
2342
|
-
|
|
2343
2401
|
// Filter out all routes below any caught error as they aren't going to
|
|
2344
2402
|
// render so we don't need to load them
|
|
2345
2403
|
function getLoaderMatchesUntilBoundary(
|
|
@@ -2507,7 +2565,7 @@ async function callLoaderOrAction(
|
|
|
2507
2565
|
request: Request,
|
|
2508
2566
|
match: AgnosticDataRouteMatch,
|
|
2509
2567
|
matches: AgnosticDataRouteMatch[],
|
|
2510
|
-
basename
|
|
2568
|
+
basename = "/",
|
|
2511
2569
|
isStaticRequest: boolean = false,
|
|
2512
2570
|
isRouteRequest: boolean = false
|
|
2513
2571
|
): Promise<DataResult> {
|
|
@@ -2531,6 +2589,13 @@ async function callLoaderOrAction(
|
|
|
2531
2589
|
handler({ request, params: match.params }),
|
|
2532
2590
|
abortPromise,
|
|
2533
2591
|
]);
|
|
2592
|
+
|
|
2593
|
+
invariant(
|
|
2594
|
+
result !== undefined,
|
|
2595
|
+
`You defined ${type === "action" ? "an action" : "a loader"} for route ` +
|
|
2596
|
+
`"${match.route.id}" but didn't return anything from your \`${type}\` ` +
|
|
2597
|
+
`function. Please return a value or \`null\`.`
|
|
2598
|
+
);
|
|
2534
2599
|
} catch (e) {
|
|
2535
2600
|
resultType = ResultType.error;
|
|
2536
2601
|
result = e;
|
|
@@ -2542,33 +2607,38 @@ async function callLoaderOrAction(
|
|
|
2542
2607
|
let status = result.status;
|
|
2543
2608
|
|
|
2544
2609
|
// Process redirects
|
|
2545
|
-
if (status
|
|
2610
|
+
if (redirectStatusCodes.has(status)) {
|
|
2546
2611
|
let location = result.headers.get("Location");
|
|
2547
2612
|
invariant(
|
|
2548
2613
|
location,
|
|
2549
2614
|
"Redirects returned/thrown from loaders/actions must have a Location header"
|
|
2550
2615
|
);
|
|
2551
2616
|
|
|
2552
|
-
//
|
|
2553
|
-
let
|
|
2554
|
-
let routePathnames = getPathContributingMatches(activeMatches).map(
|
|
2555
|
-
(match) => match.pathnameBase
|
|
2556
|
-
);
|
|
2557
|
-
let requestPath = createURL(request.url).pathname;
|
|
2558
|
-
let resolvedLocation = resolveTo(location, routePathnames, requestPath);
|
|
2559
|
-
invariant(
|
|
2560
|
-
createPath(resolvedLocation),
|
|
2561
|
-
`Unable to resolve redirect location: ${result.headers.get("Location")}`
|
|
2562
|
-
);
|
|
2617
|
+
// Check if this an external redirect that goes to a new origin
|
|
2618
|
+
let external = createURL(location).origin !== createURL("/").origin;
|
|
2563
2619
|
|
|
2564
|
-
//
|
|
2565
|
-
if (
|
|
2566
|
-
let
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2620
|
+
// Support relative routing in internal redirects
|
|
2621
|
+
if (!external) {
|
|
2622
|
+
let activeMatches = matches.slice(0, matches.indexOf(match) + 1);
|
|
2623
|
+
let routePathnames = getPathContributingMatches(activeMatches).map(
|
|
2624
|
+
(match) => match.pathnameBase
|
|
2625
|
+
);
|
|
2626
|
+
let requestPath = createURL(request.url).pathname;
|
|
2627
|
+
let resolvedLocation = resolveTo(location, routePathnames, requestPath);
|
|
2628
|
+
invariant(
|
|
2629
|
+
createPath(resolvedLocation),
|
|
2630
|
+
`Unable to resolve redirect location: ${location}`
|
|
2631
|
+
);
|
|
2570
2632
|
|
|
2571
|
-
|
|
2633
|
+
// Prepend the basename to the redirect location if we have one
|
|
2634
|
+
if (basename) {
|
|
2635
|
+
let path = resolvedLocation.pathname;
|
|
2636
|
+
resolvedLocation.pathname =
|
|
2637
|
+
path === "/" ? basename : joinPaths([basename, path]);
|
|
2638
|
+
}
|
|
2639
|
+
|
|
2640
|
+
location = createPath(resolvedLocation);
|
|
2641
|
+
}
|
|
2572
2642
|
|
|
2573
2643
|
// Don't process redirects in the router during static requests requests.
|
|
2574
2644
|
// Instead, throw the Response and let the server handle it with an HTTP
|
|
@@ -2584,6 +2654,7 @@ async function callLoaderOrAction(
|
|
|
2584
2654
|
status,
|
|
2585
2655
|
location,
|
|
2586
2656
|
revalidate: result.headers.get("X-Remix-Revalidate") !== null,
|
|
2657
|
+
external,
|
|
2587
2658
|
};
|
|
2588
2659
|
}
|
|
2589
2660
|
|
|
@@ -2851,18 +2922,13 @@ function findNearestBoundary(
|
|
|
2851
2922
|
);
|
|
2852
2923
|
}
|
|
2853
2924
|
|
|
2854
|
-
function getShortCircuitMatches(
|
|
2855
|
-
routes: AgnosticDataRouteObject[],
|
|
2856
|
-
status: number,
|
|
2857
|
-
statusText: string
|
|
2858
|
-
): {
|
|
2925
|
+
function getShortCircuitMatches(routes: AgnosticDataRouteObject[]): {
|
|
2859
2926
|
matches: AgnosticDataRouteMatch[];
|
|
2860
2927
|
route: AgnosticDataRouteObject;
|
|
2861
|
-
error: ErrorResponse;
|
|
2862
2928
|
} {
|
|
2863
2929
|
// Prefer a root layout route if present, otherwise shim in a route object
|
|
2864
2930
|
let route = routes.find((r) => r.index || !r.path || r.path === "/") || {
|
|
2865
|
-
id: `__shim
|
|
2931
|
+
id: `__shim-error-route__`,
|
|
2866
2932
|
};
|
|
2867
2933
|
|
|
2868
2934
|
return {
|
|
@@ -2875,29 +2941,60 @@ function getShortCircuitMatches(
|
|
|
2875
2941
|
},
|
|
2876
2942
|
],
|
|
2877
2943
|
route,
|
|
2878
|
-
error: new ErrorResponse(status, statusText, null),
|
|
2879
2944
|
};
|
|
2880
2945
|
}
|
|
2881
2946
|
|
|
2882
|
-
function
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2947
|
+
function getInternalRouterError(
|
|
2948
|
+
status: number,
|
|
2949
|
+
{
|
|
2950
|
+
pathname,
|
|
2951
|
+
routeId,
|
|
2952
|
+
method,
|
|
2953
|
+
message,
|
|
2954
|
+
}: {
|
|
2955
|
+
pathname?: string;
|
|
2956
|
+
routeId?: string;
|
|
2957
|
+
method?: string;
|
|
2958
|
+
message?: string;
|
|
2959
|
+
} = {}
|
|
2960
|
+
) {
|
|
2961
|
+
let statusText = "Unknown Server Error";
|
|
2962
|
+
let errorMessage = "Unknown @remix-run/router error";
|
|
2963
|
+
|
|
2964
|
+
if (status === 400) {
|
|
2965
|
+
statusText = "Bad Request";
|
|
2966
|
+
if (method && pathname && routeId) {
|
|
2967
|
+
errorMessage =
|
|
2968
|
+
`You made a ${method} request to "${pathname}" but ` +
|
|
2969
|
+
`did not provide a \`loader\` for route "${routeId}", ` +
|
|
2970
|
+
`so there is no way to handle the request.`;
|
|
2971
|
+
} else {
|
|
2972
|
+
errorMessage = "Cannot submit binary form data using GET";
|
|
2973
|
+
}
|
|
2974
|
+
} else if (status === 403) {
|
|
2975
|
+
statusText = "Forbidden";
|
|
2976
|
+
errorMessage = `Route "${routeId}" does not match URL "${pathname}"`;
|
|
2977
|
+
} else if (status === 404) {
|
|
2978
|
+
statusText = "Not Found";
|
|
2979
|
+
errorMessage = `No route matches URL "${pathname}"`;
|
|
2980
|
+
} else if (status === 405) {
|
|
2981
|
+
statusText = "Method Not Allowed";
|
|
2982
|
+
if (method && pathname && routeId) {
|
|
2983
|
+
errorMessage =
|
|
2984
|
+
`You made a ${method.toUpperCase()} request to "${pathname}" but ` +
|
|
2985
|
+
`did not provide an \`action\` for route "${routeId}", ` +
|
|
2986
|
+
`so there is no way to handle the request.`;
|
|
2987
|
+
} else if (method) {
|
|
2988
|
+
errorMessage = `Invalid request method "${method.toUpperCase()}"`;
|
|
2989
|
+
}
|
|
2990
|
+
}
|
|
2889
2991
|
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
`[${href}]`
|
|
2992
|
+
return new ErrorResponse(
|
|
2993
|
+
status || 500,
|
|
2994
|
+
statusText,
|
|
2995
|
+
new Error(errorMessage),
|
|
2996
|
+
true
|
|
2896
2997
|
);
|
|
2897
|
-
return {
|
|
2898
|
-
type: ResultType.error,
|
|
2899
|
-
error: new ErrorResponse(405, "Method Not Allowed", ""),
|
|
2900
|
-
};
|
|
2901
2998
|
}
|
|
2902
2999
|
|
|
2903
3000
|
// Find any returned redirect errors, starting from the lowest match
|
|
@@ -2951,6 +3048,14 @@ function isQueryRouteResponse(obj: any): obj is QueryRouteResponse {
|
|
|
2951
3048
|
);
|
|
2952
3049
|
}
|
|
2953
3050
|
|
|
3051
|
+
function isValidMethod(method: string): method is FormMethod {
|
|
3052
|
+
return validRequestMethods.has(method as FormMethod);
|
|
3053
|
+
}
|
|
3054
|
+
|
|
3055
|
+
function isSubmissionMethod(method: string): method is SubmissionFormMethod {
|
|
3056
|
+
return validActionMethods.has(method as SubmissionFormMethod);
|
|
3057
|
+
}
|
|
3058
|
+
|
|
2954
3059
|
async function resolveDeferredResults(
|
|
2955
3060
|
currentMatches: AgnosticDataRouteMatch[],
|
|
2956
3061
|
matchesToLoad: AgnosticDataRouteMatch[],
|