@remix-run/router 1.0.3 → 1.0.4-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 +16 -0
- package/dist/history.d.ts +3 -3
- package/dist/router.cjs.js +262 -166
- package/dist/router.cjs.js.map +1 -1
- package/dist/router.d.ts +14 -2
- package/dist/router.js +262 -166
- package/dist/router.js.map +1 -1
- package/dist/router.umd.js +262 -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 +292 -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,33 @@ 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
|
+
if (replace) {
|
|
1608
|
+
window.location.replace(redirect.location);
|
|
1609
|
+
} else {
|
|
1610
|
+
window.location.assign(redirect.location);
|
|
1611
|
+
}
|
|
1612
|
+
return;
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1569
1615
|
// There's no need to abort on redirects, since we don't detect the
|
|
1570
1616
|
// redirect until the action/loaders have settled
|
|
1571
1617
|
pendingNavigationController = null;
|
|
@@ -1573,9 +1619,40 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1573
1619
|
let redirectHistoryAction =
|
|
1574
1620
|
replace === true ? HistoryAction.Replace : HistoryAction.Push;
|
|
1575
1621
|
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1622
|
+
let { formMethod, formAction, formEncType, formData } = state.navigation;
|
|
1623
|
+
|
|
1624
|
+
// If this was a 307/308 submission we want to preserve the HTTP method and
|
|
1625
|
+
// re-submit the POST/PUT/PATCH/DELETE as a submission navigation to the
|
|
1626
|
+
// redirected location
|
|
1627
|
+
if (
|
|
1628
|
+
redirectPreserveMethodStatusCodes.has(redirect.status) &&
|
|
1629
|
+
formMethod &&
|
|
1630
|
+
isSubmissionMethod(formMethod) &&
|
|
1631
|
+
formEncType &&
|
|
1632
|
+
formData
|
|
1633
|
+
) {
|
|
1634
|
+
await startNavigation(redirectHistoryAction, redirectLocation, {
|
|
1635
|
+
submission: {
|
|
1636
|
+
formMethod,
|
|
1637
|
+
formAction: redirect.location,
|
|
1638
|
+
formEncType,
|
|
1639
|
+
formData,
|
|
1640
|
+
},
|
|
1641
|
+
});
|
|
1642
|
+
} else {
|
|
1643
|
+
// Otherwise, we kick off a new loading navigation, preserving the
|
|
1644
|
+
// submission info for the duration of this navigation
|
|
1645
|
+
await startNavigation(redirectHistoryAction, redirectLocation, {
|
|
1646
|
+
overrideNavigation: {
|
|
1647
|
+
state: "loading",
|
|
1648
|
+
location: redirectLocation,
|
|
1649
|
+
formMethod: formMethod || undefined,
|
|
1650
|
+
formAction: formAction || undefined,
|
|
1651
|
+
formEncType: formEncType || undefined,
|
|
1652
|
+
formData: formData || undefined,
|
|
1653
|
+
},
|
|
1654
|
+
});
|
|
1655
|
+
}
|
|
1579
1656
|
}
|
|
1580
1657
|
|
|
1581
1658
|
async function callLoadersAndMaybeResolveData(
|
|
@@ -1809,6 +1886,7 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1809
1886
|
// Passthrough to history-aware createHref used by useHref so we get proper
|
|
1810
1887
|
// hash-aware URLs in DOM paths
|
|
1811
1888
|
createHref: (to: To) => init.history.createHref(to),
|
|
1889
|
+
encodeLocation: (to: To) => init.history.encodeLocation(to),
|
|
1812
1890
|
getFetcher,
|
|
1813
1891
|
deleteFetcher,
|
|
1814
1892
|
dispose,
|
|
@@ -1824,11 +1902,11 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1824
1902
|
//#region createStaticHandler
|
|
1825
1903
|
////////////////////////////////////////////////////////////////////////////////
|
|
1826
1904
|
|
|
1827
|
-
const validActionMethods = new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
1828
|
-
const validRequestMethods = new Set(["GET", "HEAD", ...validActionMethods]);
|
|
1829
|
-
|
|
1830
1905
|
export function unstable_createStaticHandler(
|
|
1831
|
-
routes: AgnosticRouteObject[]
|
|
1906
|
+
routes: AgnosticRouteObject[],
|
|
1907
|
+
opts?: {
|
|
1908
|
+
basename?: string;
|
|
1909
|
+
}
|
|
1832
1910
|
): StaticHandler {
|
|
1833
1911
|
invariant(
|
|
1834
1912
|
routes.length > 0,
|
|
@@ -1836,6 +1914,7 @@ export function unstable_createStaticHandler(
|
|
|
1836
1914
|
);
|
|
1837
1915
|
|
|
1838
1916
|
let dataRoutes = convertRoutesToDataRoutes(routes);
|
|
1917
|
+
let basename = (opts ? opts.basename : null) || "/";
|
|
1839
1918
|
|
|
1840
1919
|
/**
|
|
1841
1920
|
* The query() method is intended for document requests, in which we want to
|
|
@@ -1860,16 +1939,17 @@ export function unstable_createStaticHandler(
|
|
|
1860
1939
|
request: Request
|
|
1861
1940
|
): Promise<StaticHandlerContext | Response> {
|
|
1862
1941
|
let url = new URL(request.url);
|
|
1942
|
+
let method = request.method.toLowerCase();
|
|
1863
1943
|
let location = createLocation("", createPath(url), null, "default");
|
|
1864
|
-
let matches = matchRoutes(dataRoutes, location);
|
|
1944
|
+
let matches = matchRoutes(dataRoutes, location, basename);
|
|
1865
1945
|
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
} = getMethodNotAllowedMatches(dataRoutes);
|
|
1946
|
+
// SSR supports HEAD requests while SPA doesn't
|
|
1947
|
+
if (!isValidMethod(method) && method !== "head") {
|
|
1948
|
+
let error = getInternalRouterError(405, { method });
|
|
1949
|
+
let { matches: methodNotAllowedMatches, route } =
|
|
1950
|
+
getShortCircuitMatches(dataRoutes);
|
|
1872
1951
|
return {
|
|
1952
|
+
basename,
|
|
1873
1953
|
location,
|
|
1874
1954
|
matches: methodNotAllowedMatches,
|
|
1875
1955
|
loaderData: {},
|
|
@@ -1882,12 +1962,11 @@ export function unstable_createStaticHandler(
|
|
|
1882
1962
|
actionHeaders: {},
|
|
1883
1963
|
};
|
|
1884
1964
|
} else if (!matches) {
|
|
1885
|
-
let {
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
error,
|
|
1889
|
-
} = getNotFoundMatches(dataRoutes);
|
|
1965
|
+
let error = getInternalRouterError(404, { pathname: location.pathname });
|
|
1966
|
+
let { matches: notFoundMatches, route } =
|
|
1967
|
+
getShortCircuitMatches(dataRoutes);
|
|
1890
1968
|
return {
|
|
1969
|
+
basename,
|
|
1891
1970
|
location,
|
|
1892
1971
|
matches: notFoundMatches,
|
|
1893
1972
|
loaderData: {},
|
|
@@ -1909,7 +1988,7 @@ export function unstable_createStaticHandler(
|
|
|
1909
1988
|
// When returning StaticHandlerContext, we patch back in the location here
|
|
1910
1989
|
// since we need it for React Context. But this helps keep our submit and
|
|
1911
1990
|
// loadRouteData operating on a Request instead of a Location
|
|
1912
|
-
return { location, ...result };
|
|
1991
|
+
return { location, basename, ...result };
|
|
1913
1992
|
}
|
|
1914
1993
|
|
|
1915
1994
|
/**
|
|
@@ -1925,36 +2004,38 @@ export function unstable_createStaticHandler(
|
|
|
1925
2004
|
* can do proper boundary identification in Remix where a thrown Response
|
|
1926
2005
|
* must go to the Catch Boundary but a returned Response is happy-path.
|
|
1927
2006
|
*
|
|
1928
|
-
* One thing to note is that any Router-initiated
|
|
1929
|
-
*
|
|
1930
|
-
*
|
|
2007
|
+
* One thing to note is that any Router-initiated Errors that make sense
|
|
2008
|
+
* to associate with a status code will be thrown as an ErrorResponse
|
|
2009
|
+
* instance which include the raw Error, such that the calling context can
|
|
2010
|
+
* serialize the error as they see fit while including the proper response
|
|
2011
|
+
* code. Examples here are 404 and 405 errors that occur prior to reaching
|
|
2012
|
+
* any user-defined loaders.
|
|
1931
2013
|
*/
|
|
1932
2014
|
async function queryRoute(request: Request, routeId?: string): Promise<any> {
|
|
1933
2015
|
let url = new URL(request.url);
|
|
2016
|
+
let method = request.method.toLowerCase();
|
|
1934
2017
|
let location = createLocation("", createPath(url), null, "default");
|
|
1935
|
-
let matches = matchRoutes(dataRoutes, location);
|
|
2018
|
+
let matches = matchRoutes(dataRoutes, location, basename);
|
|
1936
2019
|
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
statusText: "Method Not Allowed",
|
|
1941
|
-
});
|
|
2020
|
+
// SSR supports HEAD requests while SPA doesn't
|
|
2021
|
+
if (!isValidMethod(method) && method !== "head") {
|
|
2022
|
+
throw getInternalRouterError(405, { method });
|
|
1942
2023
|
} else if (!matches) {
|
|
1943
|
-
throw
|
|
1944
|
-
status: 404,
|
|
1945
|
-
statusText: "Not Found",
|
|
1946
|
-
});
|
|
2024
|
+
throw getInternalRouterError(404, { pathname: location.pathname });
|
|
1947
2025
|
}
|
|
1948
2026
|
|
|
1949
2027
|
let match = routeId
|
|
1950
2028
|
? matches.find((m) => m.route.id === routeId)
|
|
1951
2029
|
: getTargetMatch(matches, location);
|
|
1952
2030
|
|
|
1953
|
-
if (!match) {
|
|
1954
|
-
throw
|
|
1955
|
-
|
|
1956
|
-
|
|
2031
|
+
if (routeId && !match) {
|
|
2032
|
+
throw getInternalRouterError(403, {
|
|
2033
|
+
pathname: location.pathname,
|
|
2034
|
+
routeId,
|
|
1957
2035
|
});
|
|
2036
|
+
} else if (!match) {
|
|
2037
|
+
// This should never hit I don't think?
|
|
2038
|
+
throw getInternalRouterError(404, { pathname: location.pathname });
|
|
1958
2039
|
}
|
|
1959
2040
|
|
|
1960
2041
|
let result = await queryImpl(request, location, matches, match);
|
|
@@ -1981,14 +2062,14 @@ export function unstable_createStaticHandler(
|
|
|
1981
2062
|
location: Location,
|
|
1982
2063
|
matches: AgnosticDataRouteMatch[],
|
|
1983
2064
|
routeMatch?: AgnosticDataRouteMatch
|
|
1984
|
-
): Promise<Omit<StaticHandlerContext, "location"> | Response> {
|
|
2065
|
+
): Promise<Omit<StaticHandlerContext, "location" | "basename"> | Response> {
|
|
1985
2066
|
invariant(
|
|
1986
2067
|
request.signal,
|
|
1987
2068
|
"query()/queryRoute() requests must contain an AbortController signal"
|
|
1988
2069
|
);
|
|
1989
2070
|
|
|
1990
2071
|
try {
|
|
1991
|
-
if (
|
|
2072
|
+
if (isSubmissionMethod(request.method.toLowerCase())) {
|
|
1992
2073
|
let result = await submit(
|
|
1993
2074
|
request,
|
|
1994
2075
|
matches,
|
|
@@ -2030,23 +2111,29 @@ export function unstable_createStaticHandler(
|
|
|
2030
2111
|
matches: AgnosticDataRouteMatch[],
|
|
2031
2112
|
actionMatch: AgnosticDataRouteMatch,
|
|
2032
2113
|
isRouteRequest: boolean
|
|
2033
|
-
): Promise<Omit<StaticHandlerContext, "location"> | Response> {
|
|
2114
|
+
): Promise<Omit<StaticHandlerContext, "location" | "basename"> | Response> {
|
|
2034
2115
|
let result: DataResult;
|
|
2116
|
+
|
|
2035
2117
|
if (!actionMatch.route.action) {
|
|
2118
|
+
let error = getInternalRouterError(405, {
|
|
2119
|
+
method: request.method,
|
|
2120
|
+
pathname: createURL(request.url).pathname,
|
|
2121
|
+
routeId: actionMatch.route.id,
|
|
2122
|
+
});
|
|
2036
2123
|
if (isRouteRequest) {
|
|
2037
|
-
throw
|
|
2038
|
-
status: 405,
|
|
2039
|
-
statusText: "Method Not Allowed",
|
|
2040
|
-
});
|
|
2124
|
+
throw error;
|
|
2041
2125
|
}
|
|
2042
|
-
result =
|
|
2126
|
+
result = {
|
|
2127
|
+
type: ResultType.error,
|
|
2128
|
+
error,
|
|
2129
|
+
};
|
|
2043
2130
|
} else {
|
|
2044
2131
|
result = await callLoaderOrAction(
|
|
2045
2132
|
"action",
|
|
2046
2133
|
request,
|
|
2047
2134
|
actionMatch,
|
|
2048
2135
|
matches,
|
|
2049
|
-
|
|
2136
|
+
basename,
|
|
2050
2137
|
true,
|
|
2051
2138
|
isRouteRequest
|
|
2052
2139
|
);
|
|
@@ -2078,20 +2165,7 @@ export function unstable_createStaticHandler(
|
|
|
2078
2165
|
// Note: This should only be non-Response values if we get here, since
|
|
2079
2166
|
// isRouteRequest should throw any Response received in callLoaderOrAction
|
|
2080
2167
|
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
|
-
};
|
|
2168
|
+
throw result.error;
|
|
2095
2169
|
}
|
|
2096
2170
|
|
|
2097
2171
|
return {
|
|
@@ -2149,10 +2223,23 @@ export function unstable_createStaticHandler(
|
|
|
2149
2223
|
routeMatch?: AgnosticDataRouteMatch,
|
|
2150
2224
|
pendingActionError?: RouteData
|
|
2151
2225
|
): Promise<
|
|
2152
|
-
| Omit<
|
|
2226
|
+
| Omit<
|
|
2227
|
+
StaticHandlerContext,
|
|
2228
|
+
"location" | "basename" | "actionData" | "actionHeaders"
|
|
2229
|
+
>
|
|
2153
2230
|
| Response
|
|
2154
2231
|
> {
|
|
2155
2232
|
let isRouteRequest = routeMatch != null;
|
|
2233
|
+
|
|
2234
|
+
// Short circuit if we have no loaders to run (queryRoute())
|
|
2235
|
+
if (isRouteRequest && !routeMatch?.route.loader) {
|
|
2236
|
+
throw getInternalRouterError(400, {
|
|
2237
|
+
method: request.method,
|
|
2238
|
+
pathname: createURL(request.url).pathname,
|
|
2239
|
+
routeId: routeMatch?.route.id,
|
|
2240
|
+
});
|
|
2241
|
+
}
|
|
2242
|
+
|
|
2156
2243
|
let requestMatches = routeMatch
|
|
2157
2244
|
? [routeMatch]
|
|
2158
2245
|
: getLoaderMatchesUntilBoundary(
|
|
@@ -2161,7 +2248,7 @@ export function unstable_createStaticHandler(
|
|
|
2161
2248
|
);
|
|
2162
2249
|
let matchesToLoad = requestMatches.filter((m) => m.route.loader);
|
|
2163
2250
|
|
|
2164
|
-
// Short circuit if we have no loaders to run
|
|
2251
|
+
// Short circuit if we have no loaders to run (query())
|
|
2165
2252
|
if (matchesToLoad.length === 0) {
|
|
2166
2253
|
return {
|
|
2167
2254
|
matches,
|
|
@@ -2179,7 +2266,7 @@ export function unstable_createStaticHandler(
|
|
|
2179
2266
|
request,
|
|
2180
2267
|
match,
|
|
2181
2268
|
matches,
|
|
2182
|
-
|
|
2269
|
+
basename,
|
|
2183
2270
|
true,
|
|
2184
2271
|
isRouteRequest
|
|
2185
2272
|
)
|
|
@@ -2213,19 +2300,6 @@ export function unstable_createStaticHandler(
|
|
|
2213
2300
|
};
|
|
2214
2301
|
}
|
|
2215
2302
|
|
|
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
2303
|
return {
|
|
2230
2304
|
dataRoutes,
|
|
2231
2305
|
query,
|
|
@@ -2258,6 +2332,12 @@ export function getStaticContextFromError(
|
|
|
2258
2332
|
return newContext;
|
|
2259
2333
|
}
|
|
2260
2334
|
|
|
2335
|
+
function isSubmissionNavigation(
|
|
2336
|
+
opts: RouterNavigateOptions
|
|
2337
|
+
): opts is SubmissionNavigateOptions {
|
|
2338
|
+
return opts != null && "formData" in opts;
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2261
2341
|
// Normalize navigation options by converting formMethod=GET formData objects to
|
|
2262
2342
|
// URLSearchParams so they behave identically to links with query params
|
|
2263
2343
|
function normalizeNavigateOptions(
|
|
@@ -2272,12 +2352,19 @@ function normalizeNavigateOptions(
|
|
|
2272
2352
|
let path = typeof to === "string" ? to : createPath(to);
|
|
2273
2353
|
|
|
2274
2354
|
// Return location verbatim on non-submission navigations
|
|
2275
|
-
if (!opts ||
|
|
2355
|
+
if (!opts || !isSubmissionNavigation(opts)) {
|
|
2276
2356
|
return { path };
|
|
2277
2357
|
}
|
|
2278
2358
|
|
|
2359
|
+
if (opts.formMethod && !isValidMethod(opts.formMethod)) {
|
|
2360
|
+
return {
|
|
2361
|
+
path,
|
|
2362
|
+
error: getInternalRouterError(405, { method: opts.formMethod }),
|
|
2363
|
+
};
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2279
2366
|
// Create a Submission on non-GET navigations
|
|
2280
|
-
if (opts.formMethod
|
|
2367
|
+
if (opts.formMethod && isSubmissionMethod(opts.formMethod)) {
|
|
2281
2368
|
return {
|
|
2282
2369
|
path,
|
|
2283
2370
|
submission: {
|
|
@@ -2290,11 +2377,6 @@ function normalizeNavigateOptions(
|
|
|
2290
2377
|
};
|
|
2291
2378
|
}
|
|
2292
2379
|
|
|
2293
|
-
// No formData to flatten for GET submission
|
|
2294
|
-
if (!opts.formData) {
|
|
2295
|
-
return { path };
|
|
2296
|
-
}
|
|
2297
|
-
|
|
2298
2380
|
// Flatten submission onto URLSearchParams for GET submissions
|
|
2299
2381
|
let parsedPath = parsePath(path);
|
|
2300
2382
|
try {
|
|
@@ -2313,33 +2395,13 @@ function normalizeNavigateOptions(
|
|
|
2313
2395
|
} catch (e) {
|
|
2314
2396
|
return {
|
|
2315
2397
|
path,
|
|
2316
|
-
error:
|
|
2317
|
-
400,
|
|
2318
|
-
"Bad Request",
|
|
2319
|
-
"Cannot submit binary form data using GET"
|
|
2320
|
-
),
|
|
2398
|
+
error: getInternalRouterError(400),
|
|
2321
2399
|
};
|
|
2322
2400
|
}
|
|
2323
2401
|
|
|
2324
2402
|
return { path: createPath(parsedPath) };
|
|
2325
2403
|
}
|
|
2326
2404
|
|
|
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
2405
|
// Filter out all routes below any caught error as they aren't going to
|
|
2344
2406
|
// render so we don't need to load them
|
|
2345
2407
|
function getLoaderMatchesUntilBoundary(
|
|
@@ -2507,7 +2569,7 @@ async function callLoaderOrAction(
|
|
|
2507
2569
|
request: Request,
|
|
2508
2570
|
match: AgnosticDataRouteMatch,
|
|
2509
2571
|
matches: AgnosticDataRouteMatch[],
|
|
2510
|
-
basename
|
|
2572
|
+
basename = "/",
|
|
2511
2573
|
isStaticRequest: boolean = false,
|
|
2512
2574
|
isRouteRequest: boolean = false
|
|
2513
2575
|
): Promise<DataResult> {
|
|
@@ -2531,6 +2593,13 @@ async function callLoaderOrAction(
|
|
|
2531
2593
|
handler({ request, params: match.params }),
|
|
2532
2594
|
abortPromise,
|
|
2533
2595
|
]);
|
|
2596
|
+
|
|
2597
|
+
invariant(
|
|
2598
|
+
result !== undefined,
|
|
2599
|
+
`You defined ${type === "action" ? "an action" : "a loader"} for route ` +
|
|
2600
|
+
`"${match.route.id}" but didn't return anything from your \`${type}\` ` +
|
|
2601
|
+
`function. Please return a value or \`null\`.`
|
|
2602
|
+
);
|
|
2534
2603
|
} catch (e) {
|
|
2535
2604
|
resultType = ResultType.error;
|
|
2536
2605
|
result = e;
|
|
@@ -2542,33 +2611,38 @@ async function callLoaderOrAction(
|
|
|
2542
2611
|
let status = result.status;
|
|
2543
2612
|
|
|
2544
2613
|
// Process redirects
|
|
2545
|
-
if (status
|
|
2614
|
+
if (redirectStatusCodes.has(status)) {
|
|
2546
2615
|
let location = result.headers.get("Location");
|
|
2547
2616
|
invariant(
|
|
2548
2617
|
location,
|
|
2549
2618
|
"Redirects returned/thrown from loaders/actions must have a Location header"
|
|
2550
2619
|
);
|
|
2551
2620
|
|
|
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
|
-
);
|
|
2621
|
+
// Check if this an external redirect that goes to a new origin
|
|
2622
|
+
let external = createURL(location).origin !== createURL("/").origin;
|
|
2563
2623
|
|
|
2564
|
-
//
|
|
2565
|
-
if (
|
|
2566
|
-
let
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2624
|
+
// Support relative routing in internal redirects
|
|
2625
|
+
if (!external) {
|
|
2626
|
+
let activeMatches = matches.slice(0, matches.indexOf(match) + 1);
|
|
2627
|
+
let routePathnames = getPathContributingMatches(activeMatches).map(
|
|
2628
|
+
(match) => match.pathnameBase
|
|
2629
|
+
);
|
|
2630
|
+
let requestPath = createURL(request.url).pathname;
|
|
2631
|
+
let resolvedLocation = resolveTo(location, routePathnames, requestPath);
|
|
2632
|
+
invariant(
|
|
2633
|
+
createPath(resolvedLocation),
|
|
2634
|
+
`Unable to resolve redirect location: ${location}`
|
|
2635
|
+
);
|
|
2570
2636
|
|
|
2571
|
-
|
|
2637
|
+
// Prepend the basename to the redirect location if we have one
|
|
2638
|
+
if (basename) {
|
|
2639
|
+
let path = resolvedLocation.pathname;
|
|
2640
|
+
resolvedLocation.pathname =
|
|
2641
|
+
path === "/" ? basename : joinPaths([basename, path]);
|
|
2642
|
+
}
|
|
2643
|
+
|
|
2644
|
+
location = createPath(resolvedLocation);
|
|
2645
|
+
}
|
|
2572
2646
|
|
|
2573
2647
|
// Don't process redirects in the router during static requests requests.
|
|
2574
2648
|
// Instead, throw the Response and let the server handle it with an HTTP
|
|
@@ -2584,6 +2658,7 @@ async function callLoaderOrAction(
|
|
|
2584
2658
|
status,
|
|
2585
2659
|
location,
|
|
2586
2660
|
revalidate: result.headers.get("X-Remix-Revalidate") !== null,
|
|
2661
|
+
external,
|
|
2587
2662
|
};
|
|
2588
2663
|
}
|
|
2589
2664
|
|
|
@@ -2851,18 +2926,13 @@ function findNearestBoundary(
|
|
|
2851
2926
|
);
|
|
2852
2927
|
}
|
|
2853
2928
|
|
|
2854
|
-
function getShortCircuitMatches(
|
|
2855
|
-
routes: AgnosticDataRouteObject[],
|
|
2856
|
-
status: number,
|
|
2857
|
-
statusText: string
|
|
2858
|
-
): {
|
|
2929
|
+
function getShortCircuitMatches(routes: AgnosticDataRouteObject[]): {
|
|
2859
2930
|
matches: AgnosticDataRouteMatch[];
|
|
2860
2931
|
route: AgnosticDataRouteObject;
|
|
2861
|
-
error: ErrorResponse;
|
|
2862
2932
|
} {
|
|
2863
2933
|
// Prefer a root layout route if present, otherwise shim in a route object
|
|
2864
2934
|
let route = routes.find((r) => r.index || !r.path || r.path === "/") || {
|
|
2865
|
-
id: `__shim
|
|
2935
|
+
id: `__shim-error-route__`,
|
|
2866
2936
|
};
|
|
2867
2937
|
|
|
2868
2938
|
return {
|
|
@@ -2875,29 +2945,60 @@ function getShortCircuitMatches(
|
|
|
2875
2945
|
},
|
|
2876
2946
|
],
|
|
2877
2947
|
route,
|
|
2878
|
-
error: new ErrorResponse(status, statusText, null),
|
|
2879
2948
|
};
|
|
2880
2949
|
}
|
|
2881
2950
|
|
|
2882
|
-
function
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2951
|
+
function getInternalRouterError(
|
|
2952
|
+
status: number,
|
|
2953
|
+
{
|
|
2954
|
+
pathname,
|
|
2955
|
+
routeId,
|
|
2956
|
+
method,
|
|
2957
|
+
message,
|
|
2958
|
+
}: {
|
|
2959
|
+
pathname?: string;
|
|
2960
|
+
routeId?: string;
|
|
2961
|
+
method?: string;
|
|
2962
|
+
message?: string;
|
|
2963
|
+
} = {}
|
|
2964
|
+
) {
|
|
2965
|
+
let statusText = "Unknown Server Error";
|
|
2966
|
+
let errorMessage = "Unknown @remix-run/router error";
|
|
2967
|
+
|
|
2968
|
+
if (status === 400) {
|
|
2969
|
+
statusText = "Bad Request";
|
|
2970
|
+
if (method && pathname && routeId) {
|
|
2971
|
+
errorMessage =
|
|
2972
|
+
`You made a ${method} request to "${pathname}" but ` +
|
|
2973
|
+
`did not provide a \`loader\` for route "${routeId}", ` +
|
|
2974
|
+
`so there is no way to handle the request.`;
|
|
2975
|
+
} else {
|
|
2976
|
+
errorMessage = "Cannot submit binary form data using GET";
|
|
2977
|
+
}
|
|
2978
|
+
} else if (status === 403) {
|
|
2979
|
+
statusText = "Forbidden";
|
|
2980
|
+
errorMessage = `Route "${routeId}" does not match URL "${pathname}"`;
|
|
2981
|
+
} else if (status === 404) {
|
|
2982
|
+
statusText = "Not Found";
|
|
2983
|
+
errorMessage = `No route matches URL "${pathname}"`;
|
|
2984
|
+
} else if (status === 405) {
|
|
2985
|
+
statusText = "Method Not Allowed";
|
|
2986
|
+
if (method && pathname && routeId) {
|
|
2987
|
+
errorMessage =
|
|
2988
|
+
`You made a ${method.toUpperCase()} request to "${pathname}" but ` +
|
|
2989
|
+
`did not provide an \`action\` for route "${routeId}", ` +
|
|
2990
|
+
`so there is no way to handle the request.`;
|
|
2991
|
+
} else if (method) {
|
|
2992
|
+
errorMessage = `Invalid request method "${method.toUpperCase()}"`;
|
|
2993
|
+
}
|
|
2994
|
+
}
|
|
2889
2995
|
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
`[${href}]`
|
|
2996
|
+
return new ErrorResponse(
|
|
2997
|
+
status || 500,
|
|
2998
|
+
statusText,
|
|
2999
|
+
new Error(errorMessage),
|
|
3000
|
+
true
|
|
2896
3001
|
);
|
|
2897
|
-
return {
|
|
2898
|
-
type: ResultType.error,
|
|
2899
|
-
error: new ErrorResponse(405, "Method Not Allowed", ""),
|
|
2900
|
-
};
|
|
2901
3002
|
}
|
|
2902
3003
|
|
|
2903
3004
|
// Find any returned redirect errors, starting from the lowest match
|
|
@@ -2951,6 +3052,14 @@ function isQueryRouteResponse(obj: any): obj is QueryRouteResponse {
|
|
|
2951
3052
|
);
|
|
2952
3053
|
}
|
|
2953
3054
|
|
|
3055
|
+
function isValidMethod(method: string): method is FormMethod {
|
|
3056
|
+
return validRequestMethods.has(method as FormMethod);
|
|
3057
|
+
}
|
|
3058
|
+
|
|
3059
|
+
function isSubmissionMethod(method: string): method is SubmissionFormMethod {
|
|
3060
|
+
return validActionMethods.has(method as SubmissionFormMethod);
|
|
3061
|
+
}
|
|
3062
|
+
|
|
2954
3063
|
async function resolveDeferredResults(
|
|
2955
3064
|
currentMatches: AgnosticDataRouteMatch[],
|
|
2956
3065
|
matchesToLoad: AgnosticDataRouteMatch[],
|