@remix-run/router 1.0.2 → 1.0.3-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 +15 -2
- package/dist/index.d.ts +2 -2
- package/dist/router.cjs.js +317 -160
- package/dist/router.cjs.js.map +1 -1
- package/dist/router.js +317 -161
- package/dist/router.js.map +1 -1
- package/dist/router.umd.js +3411 -0
- package/dist/router.umd.js.map +1 -0
- package/dist/router.umd.min.js +12 -0
- package/dist/router.umd.min.js.map +1 -0
- package/dist/utils.d.ts +24 -0
- package/history.ts +2 -2
- package/index.ts +5 -2
- package/package.json +2 -1
- package/router.ts +375 -158
- package/utils.ts +54 -1
package/router.ts
CHANGED
|
@@ -25,9 +25,12 @@ import {
|
|
|
25
25
|
ErrorResponse,
|
|
26
26
|
ResultType,
|
|
27
27
|
convertRoutesToDataRoutes,
|
|
28
|
+
getPathContributingMatches,
|
|
28
29
|
invariant,
|
|
29
30
|
isRouteErrorResponse,
|
|
31
|
+
joinPaths,
|
|
30
32
|
matchRoutes,
|
|
33
|
+
resolveTo,
|
|
31
34
|
} from "./utils";
|
|
32
35
|
|
|
33
36
|
////////////////////////////////////////////////////////////////////////////////
|
|
@@ -469,14 +472,35 @@ interface HandleLoadersResult extends ShortCircuitable {
|
|
|
469
472
|
}
|
|
470
473
|
|
|
471
474
|
/**
|
|
472
|
-
* Tuple of [key, href,
|
|
475
|
+
* Tuple of [key, href, DataRouteMatch, DataRouteMatch[]] for a revalidating
|
|
476
|
+
* fetcher.load()
|
|
473
477
|
*/
|
|
474
|
-
type RevalidatingFetcher = [
|
|
478
|
+
type RevalidatingFetcher = [
|
|
479
|
+
string,
|
|
480
|
+
string,
|
|
481
|
+
AgnosticDataRouteMatch,
|
|
482
|
+
AgnosticDataRouteMatch[]
|
|
483
|
+
];
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Tuple of [href, DataRouteMatch, DataRouteMatch[]] for an active
|
|
487
|
+
* fetcher.load()
|
|
488
|
+
*/
|
|
489
|
+
type FetchLoadMatch = [
|
|
490
|
+
string,
|
|
491
|
+
AgnosticDataRouteMatch,
|
|
492
|
+
AgnosticDataRouteMatch[]
|
|
493
|
+
];
|
|
475
494
|
|
|
476
495
|
/**
|
|
477
|
-
*
|
|
496
|
+
* Wrapper object to allow us to throw any response out from callLoaderOrAction
|
|
497
|
+
* for queryRouter while preserving whether or not it was thrown or returned
|
|
498
|
+
* from the loader/action
|
|
478
499
|
*/
|
|
479
|
-
|
|
500
|
+
interface QueryRouteResponse {
|
|
501
|
+
type: ResultType.data | ResultType.error;
|
|
502
|
+
response: Response;
|
|
503
|
+
}
|
|
480
504
|
|
|
481
505
|
export const IDLE_NAVIGATION: NavigationStates["Idle"] = {
|
|
482
506
|
state: "idle",
|
|
@@ -495,6 +519,12 @@ export const IDLE_FETCHER: FetcherStates["Idle"] = {
|
|
|
495
519
|
formEncType: undefined,
|
|
496
520
|
formData: undefined,
|
|
497
521
|
};
|
|
522
|
+
|
|
523
|
+
const isBrowser =
|
|
524
|
+
typeof window !== "undefined" &&
|
|
525
|
+
typeof window.document !== "undefined" &&
|
|
526
|
+
typeof window.document.createElement !== "undefined";
|
|
527
|
+
const isServer = !isBrowser;
|
|
498
528
|
//#endregion
|
|
499
529
|
|
|
500
530
|
////////////////////////////////////////////////////////////////////////////////
|
|
@@ -733,6 +763,20 @@ export function createRouter(init: RouterInit): Router {
|
|
|
733
763
|
let { path, submission, error } = normalizeNavigateOptions(to, opts);
|
|
734
764
|
|
|
735
765
|
let location = createLocation(state.location, path, opts && opts.state);
|
|
766
|
+
|
|
767
|
+
// When using navigate as a PUSH/REPLACE we aren't reading an already-encoded
|
|
768
|
+
// URL from window.location, so we need to encode it here so the behavior
|
|
769
|
+
// remains the same as POP and non-data-router usages. new URL() does all
|
|
770
|
+
// the same encoding we'd get from a history.pushState/window.location read
|
|
771
|
+
// without having to touch history
|
|
772
|
+
let url = createURL(createPath(location));
|
|
773
|
+
location = {
|
|
774
|
+
...location,
|
|
775
|
+
pathname: url.pathname,
|
|
776
|
+
search: url.search,
|
|
777
|
+
hash: url.hash,
|
|
778
|
+
};
|
|
779
|
+
|
|
736
780
|
let historyAction =
|
|
737
781
|
(opts && opts.replace) === true || submission != null
|
|
738
782
|
? HistoryAction.Replace
|
|
@@ -939,7 +983,13 @@ export function createRouter(init: RouterInit): Router {
|
|
|
939
983
|
if (!actionMatch.route.action) {
|
|
940
984
|
result = getMethodNotAllowedResult(location);
|
|
941
985
|
} else {
|
|
942
|
-
result = await callLoaderOrAction(
|
|
986
|
+
result = await callLoaderOrAction(
|
|
987
|
+
"action",
|
|
988
|
+
request,
|
|
989
|
+
actionMatch,
|
|
990
|
+
matches,
|
|
991
|
+
router.basename
|
|
992
|
+
);
|
|
943
993
|
|
|
944
994
|
if (request.signal.aborted) {
|
|
945
995
|
return { shortCircuited: true };
|
|
@@ -1053,7 +1103,7 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1053
1103
|
// a revalidation interrupting an actionReload)
|
|
1054
1104
|
if (!isUninterruptedRevalidation) {
|
|
1055
1105
|
revalidatingFetchers.forEach(([key]) => {
|
|
1056
|
-
|
|
1106
|
+
let fetcher = state.fetchers.get(key);
|
|
1057
1107
|
let revalidatingFetcher: FetcherStates["Loading"] = {
|
|
1058
1108
|
state: "loading",
|
|
1059
1109
|
data: fetcher && fetcher.data,
|
|
@@ -1081,6 +1131,7 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1081
1131
|
let { results, loaderResults, fetcherResults } =
|
|
1082
1132
|
await callLoadersAndMaybeResolveData(
|
|
1083
1133
|
state.matches,
|
|
1134
|
+
matches,
|
|
1084
1135
|
matchesToLoad,
|
|
1085
1136
|
revalidatingFetchers,
|
|
1086
1137
|
request
|
|
@@ -1150,7 +1201,7 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1150
1201
|
href: string,
|
|
1151
1202
|
opts?: RouterFetchOptions
|
|
1152
1203
|
) {
|
|
1153
|
-
if (
|
|
1204
|
+
if (isServer) {
|
|
1154
1205
|
throw new Error(
|
|
1155
1206
|
"router.fetch() was called during the server render, but it shouldn't be. " +
|
|
1156
1207
|
"You are likely calling a useFetcher() method in the body of your component. " +
|
|
@@ -1170,14 +1221,14 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1170
1221
|
let match = getTargetMatch(matches, path);
|
|
1171
1222
|
|
|
1172
1223
|
if (submission) {
|
|
1173
|
-
handleFetcherAction(key, routeId, path, match, submission);
|
|
1224
|
+
handleFetcherAction(key, routeId, path, match, matches, submission);
|
|
1174
1225
|
return;
|
|
1175
1226
|
}
|
|
1176
1227
|
|
|
1177
1228
|
// Store off the match so we can call it's shouldRevalidate on subsequent
|
|
1178
1229
|
// revalidations
|
|
1179
|
-
fetchLoadMatches.set(key, [path, match]);
|
|
1180
|
-
handleFetcherLoader(key, routeId, path, match);
|
|
1230
|
+
fetchLoadMatches.set(key, [path, match, matches]);
|
|
1231
|
+
handleFetcherLoader(key, routeId, path, match, matches);
|
|
1181
1232
|
}
|
|
1182
1233
|
|
|
1183
1234
|
// Call the action for the matched fetcher.submit(), and then handle redirects,
|
|
@@ -1187,6 +1238,7 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1187
1238
|
routeId: string,
|
|
1188
1239
|
path: string,
|
|
1189
1240
|
match: AgnosticDataRouteMatch,
|
|
1241
|
+
requestMatches: AgnosticDataRouteMatch[],
|
|
1190
1242
|
submission: Submission
|
|
1191
1243
|
) {
|
|
1192
1244
|
interruptActiveLoads();
|
|
@@ -1213,7 +1265,13 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1213
1265
|
let fetchRequest = createRequest(path, abortController.signal, submission);
|
|
1214
1266
|
fetchControllers.set(key, abortController);
|
|
1215
1267
|
|
|
1216
|
-
let actionResult = await callLoaderOrAction(
|
|
1268
|
+
let actionResult = await callLoaderOrAction(
|
|
1269
|
+
"action",
|
|
1270
|
+
fetchRequest,
|
|
1271
|
+
match,
|
|
1272
|
+
requestMatches,
|
|
1273
|
+
router.basename
|
|
1274
|
+
);
|
|
1217
1275
|
|
|
1218
1276
|
if (fetchRequest.signal.aborted) {
|
|
1219
1277
|
// We can delete this so long as we weren't aborted by ou our own fetcher
|
|
@@ -1315,6 +1373,7 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1315
1373
|
let { results, loaderResults, fetcherResults } =
|
|
1316
1374
|
await callLoadersAndMaybeResolveData(
|
|
1317
1375
|
state.matches,
|
|
1376
|
+
matches,
|
|
1318
1377
|
matchesToLoad,
|
|
1319
1378
|
revalidatingFetchers,
|
|
1320
1379
|
revalidationRequest
|
|
@@ -1395,7 +1454,8 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1395
1454
|
key: string,
|
|
1396
1455
|
routeId: string,
|
|
1397
1456
|
path: string,
|
|
1398
|
-
match: AgnosticDataRouteMatch
|
|
1457
|
+
match: AgnosticDataRouteMatch,
|
|
1458
|
+
matches: AgnosticDataRouteMatch[]
|
|
1399
1459
|
) {
|
|
1400
1460
|
let existingFetcher = state.fetchers.get(key);
|
|
1401
1461
|
// Put this fetcher into it's loading state
|
|
@@ -1417,7 +1477,9 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1417
1477
|
let result: DataResult = await callLoaderOrAction(
|
|
1418
1478
|
"loader",
|
|
1419
1479
|
fetchRequest,
|
|
1420
|
-
match
|
|
1480
|
+
match,
|
|
1481
|
+
matches,
|
|
1482
|
+
router.basename
|
|
1421
1483
|
);
|
|
1422
1484
|
|
|
1423
1485
|
// Deferred isn't supported or fetcher loads, await everything and treat it
|
|
@@ -1515,6 +1577,7 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1515
1577
|
|
|
1516
1578
|
let redirectHistoryAction =
|
|
1517
1579
|
replace === true ? HistoryAction.Replace : HistoryAction.Push;
|
|
1580
|
+
|
|
1518
1581
|
await startNavigation(redirectHistoryAction, navigation.location, {
|
|
1519
1582
|
overrideNavigation: navigation,
|
|
1520
1583
|
});
|
|
@@ -1522,6 +1585,7 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1522
1585
|
|
|
1523
1586
|
async function callLoadersAndMaybeResolveData(
|
|
1524
1587
|
currentMatches: AgnosticDataRouteMatch[],
|
|
1588
|
+
matches: AgnosticDataRouteMatch[],
|
|
1525
1589
|
matchesToLoad: AgnosticDataRouteMatch[],
|
|
1526
1590
|
fetchersToLoad: RevalidatingFetcher[],
|
|
1527
1591
|
request: Request
|
|
@@ -1530,9 +1594,17 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1530
1594
|
// then slice off the results into separate arrays so we can handle them
|
|
1531
1595
|
// accordingly
|
|
1532
1596
|
let results = await Promise.all([
|
|
1533
|
-
...matchesToLoad.map((
|
|
1534
|
-
|
|
1535
|
-
|
|
1597
|
+
...matchesToLoad.map((match) =>
|
|
1598
|
+
callLoaderOrAction("loader", request, match, matches, router.basename)
|
|
1599
|
+
),
|
|
1600
|
+
...fetchersToLoad.map(([, href, match, fetchMatches]) =>
|
|
1601
|
+
callLoaderOrAction(
|
|
1602
|
+
"loader",
|
|
1603
|
+
createRequest(href, request.signal),
|
|
1604
|
+
match,
|
|
1605
|
+
fetchMatches,
|
|
1606
|
+
router.basename
|
|
1607
|
+
)
|
|
1536
1608
|
),
|
|
1537
1609
|
]);
|
|
1538
1610
|
let loaderResults = results.slice(0, matchesToLoad.length);
|
|
@@ -1739,7 +1811,9 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1739
1811
|
navigate,
|
|
1740
1812
|
fetch,
|
|
1741
1813
|
revalidate,
|
|
1742
|
-
createHref
|
|
1814
|
+
// Passthrough to history-aware createHref used by useHref so we get proper
|
|
1815
|
+
// hash-aware URLs in DOM paths
|
|
1816
|
+
createHref: (to: To) => init.history.createHref(to),
|
|
1743
1817
|
getFetcher,
|
|
1744
1818
|
deleteFetcher,
|
|
1745
1819
|
dispose,
|
|
@@ -1755,6 +1829,9 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1755
1829
|
//#region createStaticHandler
|
|
1756
1830
|
////////////////////////////////////////////////////////////////////////////////
|
|
1757
1831
|
|
|
1832
|
+
const validActionMethods = new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
1833
|
+
const validRequestMethods = new Set(["GET", "HEAD", ...validActionMethods]);
|
|
1834
|
+
|
|
1758
1835
|
export function unstable_createStaticHandler(
|
|
1759
1836
|
routes: AgnosticRouteObject[]
|
|
1760
1837
|
): StaticHandler {
|
|
@@ -1765,37 +1842,133 @@ export function unstable_createStaticHandler(
|
|
|
1765
1842
|
|
|
1766
1843
|
let dataRoutes = convertRoutesToDataRoutes(routes);
|
|
1767
1844
|
|
|
1845
|
+
/**
|
|
1846
|
+
* The query() method is intended for document requests, in which we want to
|
|
1847
|
+
* call an optional action and potentially multiple loaders for all nested
|
|
1848
|
+
* routes. It returns a StaticHandlerContext object, which is very similar
|
|
1849
|
+
* to the router state (location, loaderData, actionData, errors, etc.) and
|
|
1850
|
+
* also adds SSR-specific information such as the statusCode and headers
|
|
1851
|
+
* from action/loaders Responses.
|
|
1852
|
+
*
|
|
1853
|
+
* It _should_ never throw and should report all errors through the
|
|
1854
|
+
* returned context.errors object, properly associating errors to their error
|
|
1855
|
+
* boundary. Additionally, it tracks _deepestRenderedBoundaryId which can be
|
|
1856
|
+
* used to emulate React error boundaries during SSr by performing a second
|
|
1857
|
+
* pass only down to the boundaryId.
|
|
1858
|
+
*
|
|
1859
|
+
* The one exception where we do not return a StaticHandlerContext is when a
|
|
1860
|
+
* redirect response is returned or thrown from any action/loader. We
|
|
1861
|
+
* propagate that out and return the raw Response so the HTTP server can
|
|
1862
|
+
* return it directly.
|
|
1863
|
+
*/
|
|
1768
1864
|
async function query(
|
|
1769
1865
|
request: Request
|
|
1770
1866
|
): Promise<StaticHandlerContext | Response> {
|
|
1771
|
-
let
|
|
1867
|
+
let url = new URL(request.url);
|
|
1868
|
+
let location = createLocation("", createPath(url), null, "default");
|
|
1869
|
+
let matches = matchRoutes(dataRoutes, location);
|
|
1870
|
+
|
|
1871
|
+
if (!validRequestMethods.has(request.method)) {
|
|
1872
|
+
let {
|
|
1873
|
+
matches: methodNotAllowedMatches,
|
|
1874
|
+
route,
|
|
1875
|
+
error,
|
|
1876
|
+
} = getMethodNotAllowedMatches(dataRoutes);
|
|
1877
|
+
return {
|
|
1878
|
+
location,
|
|
1879
|
+
matches: methodNotAllowedMatches,
|
|
1880
|
+
loaderData: {},
|
|
1881
|
+
actionData: null,
|
|
1882
|
+
errors: {
|
|
1883
|
+
[route.id]: error,
|
|
1884
|
+
},
|
|
1885
|
+
statusCode: error.status,
|
|
1886
|
+
loaderHeaders: {},
|
|
1887
|
+
actionHeaders: {},
|
|
1888
|
+
};
|
|
1889
|
+
} else if (!matches) {
|
|
1890
|
+
let {
|
|
1891
|
+
matches: notFoundMatches,
|
|
1892
|
+
route,
|
|
1893
|
+
error,
|
|
1894
|
+
} = getNotFoundMatches(dataRoutes);
|
|
1895
|
+
return {
|
|
1896
|
+
location,
|
|
1897
|
+
matches: notFoundMatches,
|
|
1898
|
+
loaderData: {},
|
|
1899
|
+
actionData: null,
|
|
1900
|
+
errors: {
|
|
1901
|
+
[route.id]: error,
|
|
1902
|
+
},
|
|
1903
|
+
statusCode: error.status,
|
|
1904
|
+
loaderHeaders: {},
|
|
1905
|
+
actionHeaders: {},
|
|
1906
|
+
};
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
let result = await queryImpl(request, location, matches);
|
|
1772
1910
|
if (result instanceof Response) {
|
|
1773
1911
|
return result;
|
|
1774
1912
|
}
|
|
1913
|
+
|
|
1775
1914
|
// When returning StaticHandlerContext, we patch back in the location here
|
|
1776
1915
|
// since we need it for React Context. But this helps keep our submit and
|
|
1777
1916
|
// loadRouteData operating on a Request instead of a Location
|
|
1778
1917
|
return { location, ...result };
|
|
1779
1918
|
}
|
|
1780
1919
|
|
|
1781
|
-
|
|
1782
|
-
|
|
1920
|
+
/**
|
|
1921
|
+
* The queryRoute() method is intended for targeted route requests, either
|
|
1922
|
+
* for fetch ?_data requests or resource route requests. In this case, we
|
|
1923
|
+
* are only ever calling a single action or loader, and we are returning the
|
|
1924
|
+
* returned value directly. In most cases, this will be a Response returned
|
|
1925
|
+
* from the action/loader, but it may be a primitive or other value as well -
|
|
1926
|
+
* and in such cases the calling context should handle that accordingly.
|
|
1927
|
+
*
|
|
1928
|
+
* We do respect the throw/return differentiation, so if an action/loader
|
|
1929
|
+
* throws, then this method will throw the value. This is important so we
|
|
1930
|
+
* can do proper boundary identification in Remix where a thrown Response
|
|
1931
|
+
* must go to the Catch Boundary but a returned Response is happy-path.
|
|
1932
|
+
*
|
|
1933
|
+
* One thing to note is that any Router-initiated thrown Response (such as a
|
|
1934
|
+
* 404 or 405) will have a custom X-Remix-Router-Error: "yes" header on it
|
|
1935
|
+
* in order to differentiate from responses thrown from user actions/loaders.
|
|
1936
|
+
*/
|
|
1937
|
+
async function queryRoute(request: Request, routeId?: string): Promise<any> {
|
|
1938
|
+
let url = new URL(request.url);
|
|
1939
|
+
let location = createLocation("", createPath(url), null, "default");
|
|
1940
|
+
let matches = matchRoutes(dataRoutes, location);
|
|
1941
|
+
|
|
1942
|
+
if (!validRequestMethods.has(request.method)) {
|
|
1943
|
+
throw createRouterErrorResponse(null, {
|
|
1944
|
+
status: 405,
|
|
1945
|
+
statusText: "Method Not Allowed",
|
|
1946
|
+
});
|
|
1947
|
+
} else if (!matches) {
|
|
1948
|
+
throw createRouterErrorResponse(null, {
|
|
1949
|
+
status: 404,
|
|
1950
|
+
statusText: "Not Found",
|
|
1951
|
+
});
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
let match = routeId
|
|
1955
|
+
? matches.find((m) => m.route.id === routeId)
|
|
1956
|
+
: getTargetMatch(matches, location);
|
|
1957
|
+
|
|
1958
|
+
if (!match) {
|
|
1959
|
+
throw createRouterErrorResponse(null, {
|
|
1960
|
+
status: 404,
|
|
1961
|
+
statusText: "Not Found",
|
|
1962
|
+
});
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
let result = await queryImpl(request, location, matches, match);
|
|
1783
1966
|
if (result instanceof Response) {
|
|
1784
1967
|
return result;
|
|
1785
1968
|
}
|
|
1786
1969
|
|
|
1787
1970
|
let error = result.errors ? Object.values(result.errors)[0] : undefined;
|
|
1788
1971
|
if (error !== undefined) {
|
|
1789
|
-
// While we always re-throw Responses returned from loaders/actions
|
|
1790
|
-
// directly for route requests and prevent the unwrapping into an
|
|
1791
|
-
// ErrorResponse, we still need this for error cases _prior_ the
|
|
1792
|
-
// execution of the loader/action, such as a 404/405 error.
|
|
1793
|
-
if (isRouteErrorResponse(error)) {
|
|
1794
|
-
return new Response(error.data, {
|
|
1795
|
-
status: error.status,
|
|
1796
|
-
statusText: error.statusText,
|
|
1797
|
-
});
|
|
1798
|
-
}
|
|
1799
1972
|
// If we got back result.errors, that means the loader/action threw
|
|
1800
1973
|
// _something_ that wasn't a Response, but it's not guaranteed/required
|
|
1801
1974
|
// to be an `instanceof Error` either, so we have to use throw here to
|
|
@@ -1805,66 +1978,53 @@ export function unstable_createStaticHandler(
|
|
|
1805
1978
|
|
|
1806
1979
|
// Pick off the right state value to return
|
|
1807
1980
|
let routeData = [result.actionData, result.loaderData].find((v) => v);
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
if (isRouteErrorResponse(value)) {
|
|
1811
|
-
return new Response(value.data, {
|
|
1812
|
-
status: value.status,
|
|
1813
|
-
statusText: value.statusText,
|
|
1814
|
-
});
|
|
1815
|
-
}
|
|
1816
|
-
|
|
1817
|
-
return value;
|
|
1981
|
+
return Object.values(routeData || {})[0];
|
|
1818
1982
|
}
|
|
1819
1983
|
|
|
1820
1984
|
async function queryImpl(
|
|
1821
1985
|
request: Request,
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
}> {
|
|
1827
|
-
invariant(
|
|
1828
|
-
request.method !== "HEAD",
|
|
1829
|
-
"query()/queryRoute() do not support HEAD requests"
|
|
1830
|
-
);
|
|
1986
|
+
location: Location,
|
|
1987
|
+
matches: AgnosticDataRouteMatch[],
|
|
1988
|
+
routeMatch?: AgnosticDataRouteMatch
|
|
1989
|
+
): Promise<Omit<StaticHandlerContext, "location"> | Response> {
|
|
1831
1990
|
invariant(
|
|
1832
1991
|
request.signal,
|
|
1833
1992
|
"query()/queryRoute() requests must contain an AbortController signal"
|
|
1834
1993
|
);
|
|
1835
1994
|
|
|
1836
|
-
let { location, matches, shortCircuitState } = matchRequest(
|
|
1837
|
-
request,
|
|
1838
|
-
routeId
|
|
1839
|
-
);
|
|
1840
|
-
|
|
1841
1995
|
try {
|
|
1842
|
-
if (
|
|
1843
|
-
return { location, result: shortCircuitState };
|
|
1844
|
-
}
|
|
1845
|
-
|
|
1846
|
-
if (request.method !== "GET") {
|
|
1996
|
+
if (validActionMethods.has(request.method)) {
|
|
1847
1997
|
let result = await submit(
|
|
1848
1998
|
request,
|
|
1849
1999
|
matches,
|
|
1850
|
-
getTargetMatch(matches, location),
|
|
1851
|
-
|
|
2000
|
+
routeMatch || getTargetMatch(matches, location),
|
|
2001
|
+
routeMatch != null
|
|
1852
2002
|
);
|
|
1853
|
-
return
|
|
2003
|
+
return result;
|
|
1854
2004
|
}
|
|
1855
2005
|
|
|
1856
|
-
let result = await loadRouteData(request, matches,
|
|
1857
|
-
return
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
};
|
|
2006
|
+
let result = await loadRouteData(request, matches, routeMatch);
|
|
2007
|
+
return result instanceof Response
|
|
2008
|
+
? result
|
|
2009
|
+
: {
|
|
2010
|
+
...result,
|
|
2011
|
+
actionData: null,
|
|
2012
|
+
actionHeaders: {},
|
|
2013
|
+
};
|
|
1865
2014
|
} catch (e) {
|
|
1866
|
-
|
|
1867
|
-
|
|
2015
|
+
// If the user threw/returned a Response in callLoaderOrAction, we throw
|
|
2016
|
+
// it to bail out and then return or throw here based on whether the user
|
|
2017
|
+
// returned or threw
|
|
2018
|
+
if (isQueryRouteResponse(e)) {
|
|
2019
|
+
if (e.type === ResultType.error && !isRedirectResponse(e.response)) {
|
|
2020
|
+
throw e.response;
|
|
2021
|
+
}
|
|
2022
|
+
return e.response;
|
|
2023
|
+
}
|
|
2024
|
+
// Redirects are always returned since they don't propagate to catch
|
|
2025
|
+
// boundaries
|
|
2026
|
+
if (isRedirectResponse(e)) {
|
|
2027
|
+
return e;
|
|
1868
2028
|
}
|
|
1869
2029
|
throw e;
|
|
1870
2030
|
}
|
|
@@ -1878,13 +2038,21 @@ export function unstable_createStaticHandler(
|
|
|
1878
2038
|
): Promise<Omit<StaticHandlerContext, "location"> | Response> {
|
|
1879
2039
|
let result: DataResult;
|
|
1880
2040
|
if (!actionMatch.route.action) {
|
|
1881
|
-
let href =
|
|
2041
|
+
let href = createServerHref(new URL(request.url));
|
|
2042
|
+
if (isRouteRequest) {
|
|
2043
|
+
throw createRouterErrorResponse(null, {
|
|
2044
|
+
status: 405,
|
|
2045
|
+
statusText: "Method Not Allowed",
|
|
2046
|
+
});
|
|
2047
|
+
}
|
|
1882
2048
|
result = getMethodNotAllowedResult(href);
|
|
1883
2049
|
} else {
|
|
1884
2050
|
result = await callLoaderOrAction(
|
|
1885
2051
|
"action",
|
|
1886
2052
|
request,
|
|
1887
2053
|
actionMatch,
|
|
2054
|
+
matches,
|
|
2055
|
+
undefined, // Basename not currently supported in static handlers
|
|
1888
2056
|
true,
|
|
1889
2057
|
isRouteRequest
|
|
1890
2058
|
);
|
|
@@ -1897,7 +2065,7 @@ export function unstable_createStaticHandler(
|
|
|
1897
2065
|
|
|
1898
2066
|
if (isRedirectResult(result)) {
|
|
1899
2067
|
// Uhhhh - this should never happen, we should always throw these from
|
|
1900
|
-
//
|
|
2068
|
+
// callLoaderOrAction, but the type narrowing here keeps TS happy and we
|
|
1901
2069
|
// can get back on the "throw all redirect responses" train here should
|
|
1902
2070
|
// this ever happen :/
|
|
1903
2071
|
throw new Response(null, {
|
|
@@ -1913,6 +2081,8 @@ export function unstable_createStaticHandler(
|
|
|
1913
2081
|
}
|
|
1914
2082
|
|
|
1915
2083
|
if (isRouteRequest) {
|
|
2084
|
+
// Note: This should only be non-Response values if we get here, since
|
|
2085
|
+
// isRouteRequest should throw any Response received in callLoaderOrAction
|
|
1916
2086
|
if (isErrorResult(result)) {
|
|
1917
2087
|
let boundaryMatch = findNearestBoundary(matches, actionMatch.route.id);
|
|
1918
2088
|
return {
|
|
@@ -1947,7 +2117,7 @@ export function unstable_createStaticHandler(
|
|
|
1947
2117
|
// Store off the pending error - we use it to determine which loaders
|
|
1948
2118
|
// to call and will commit it when we complete the navigation
|
|
1949
2119
|
let boundaryMatch = findNearestBoundary(matches, actionMatch.route.id);
|
|
1950
|
-
let context = await loadRouteData(request, matches,
|
|
2120
|
+
let context = await loadRouteData(request, matches, undefined, {
|
|
1951
2121
|
[boundaryMatch.route.id]: result.error,
|
|
1952
2122
|
});
|
|
1953
2123
|
|
|
@@ -1964,7 +2134,7 @@ export function unstable_createStaticHandler(
|
|
|
1964
2134
|
};
|
|
1965
2135
|
}
|
|
1966
2136
|
|
|
1967
|
-
let context = await loadRouteData(request, matches
|
|
2137
|
+
let context = await loadRouteData(request, matches);
|
|
1968
2138
|
|
|
1969
2139
|
return {
|
|
1970
2140
|
...context,
|
|
@@ -1982,16 +2152,20 @@ export function unstable_createStaticHandler(
|
|
|
1982
2152
|
async function loadRouteData(
|
|
1983
2153
|
request: Request,
|
|
1984
2154
|
matches: AgnosticDataRouteMatch[],
|
|
1985
|
-
|
|
2155
|
+
routeMatch?: AgnosticDataRouteMatch,
|
|
1986
2156
|
pendingActionError?: RouteData
|
|
1987
2157
|
): Promise<
|
|
1988
2158
|
| Omit<StaticHandlerContext, "location" | "actionData" | "actionHeaders">
|
|
1989
2159
|
| Response
|
|
1990
2160
|
> {
|
|
1991
|
-
let
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
2161
|
+
let isRouteRequest = routeMatch != null;
|
|
2162
|
+
let requestMatches = routeMatch
|
|
2163
|
+
? [routeMatch]
|
|
2164
|
+
: getLoaderMatchesUntilBoundary(
|
|
2165
|
+
matches,
|
|
2166
|
+
Object.keys(pendingActionError || {})[0]
|
|
2167
|
+
);
|
|
2168
|
+
let matchesToLoad = requestMatches.filter((m) => m.route.loader);
|
|
1995
2169
|
|
|
1996
2170
|
// Short circuit if we have no loaders to run
|
|
1997
2171
|
if (matchesToLoad.length === 0) {
|
|
@@ -2005,8 +2179,16 @@ export function unstable_createStaticHandler(
|
|
|
2005
2179
|
}
|
|
2006
2180
|
|
|
2007
2181
|
let results = await Promise.all([
|
|
2008
|
-
...matchesToLoad.map((
|
|
2009
|
-
callLoaderOrAction(
|
|
2182
|
+
...matchesToLoad.map((match) =>
|
|
2183
|
+
callLoaderOrAction(
|
|
2184
|
+
"loader",
|
|
2185
|
+
request,
|
|
2186
|
+
match,
|
|
2187
|
+
matches,
|
|
2188
|
+
undefined, // Basename not currently supported in static handlers
|
|
2189
|
+
true,
|
|
2190
|
+
isRouteRequest
|
|
2191
|
+
)
|
|
2010
2192
|
),
|
|
2011
2193
|
]);
|
|
2012
2194
|
|
|
@@ -2037,47 +2219,17 @@ export function unstable_createStaticHandler(
|
|
|
2037
2219
|
};
|
|
2038
2220
|
}
|
|
2039
2221
|
|
|
2040
|
-
function
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
)
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
let matches = matchRoutes(dataRoutes, location);
|
|
2052
|
-
if (matches && routeId) {
|
|
2053
|
-
matches = matches.filter((m) => m.route.id === routeId);
|
|
2054
|
-
}
|
|
2055
|
-
|
|
2056
|
-
// Short circuit with a 404 if we match nothing
|
|
2057
|
-
if (!matches) {
|
|
2058
|
-
let {
|
|
2059
|
-
matches: notFoundMatches,
|
|
2060
|
-
route,
|
|
2061
|
-
error,
|
|
2062
|
-
} = getNotFoundMatches(dataRoutes);
|
|
2063
|
-
return {
|
|
2064
|
-
location,
|
|
2065
|
-
matches: notFoundMatches,
|
|
2066
|
-
shortCircuitState: {
|
|
2067
|
-
matches: notFoundMatches,
|
|
2068
|
-
loaderData: {},
|
|
2069
|
-
actionData: null,
|
|
2070
|
-
errors: {
|
|
2071
|
-
[route.id]: error,
|
|
2072
|
-
},
|
|
2073
|
-
statusCode: 404,
|
|
2074
|
-
loaderHeaders: {},
|
|
2075
|
-
actionHeaders: {},
|
|
2076
|
-
},
|
|
2077
|
-
};
|
|
2078
|
-
}
|
|
2079
|
-
|
|
2080
|
-
return { location, matches };
|
|
2222
|
+
function createRouterErrorResponse(
|
|
2223
|
+
body: BodyInit | null | undefined,
|
|
2224
|
+
init: ResponseInit
|
|
2225
|
+
) {
|
|
2226
|
+
return new Response(body, {
|
|
2227
|
+
...init,
|
|
2228
|
+
headers: {
|
|
2229
|
+
...init.headers,
|
|
2230
|
+
"X-Remix-Router-Error": "yes",
|
|
2231
|
+
},
|
|
2232
|
+
});
|
|
2081
2233
|
}
|
|
2082
2234
|
|
|
2083
2235
|
return {
|
|
@@ -2136,7 +2288,7 @@ function normalizeNavigateOptions(
|
|
|
2136
2288
|
path,
|
|
2137
2289
|
submission: {
|
|
2138
2290
|
formMethod: opts.formMethod,
|
|
2139
|
-
formAction:
|
|
2291
|
+
formAction: createServerHref(parsePath(path)),
|
|
2140
2292
|
formEncType:
|
|
2141
2293
|
(opts && opts.formEncType) || "application/x-www-form-urlencoded",
|
|
2142
2294
|
formData: opts.formData,
|
|
@@ -2251,10 +2403,10 @@ function getMatchesToLoad(
|
|
|
2251
2403
|
// Pick fetcher.loads that need to be revalidated
|
|
2252
2404
|
let revalidatingFetchers: RevalidatingFetcher[] = [];
|
|
2253
2405
|
fetchLoadMatches &&
|
|
2254
|
-
fetchLoadMatches.forEach(([href, match], key) => {
|
|
2406
|
+
fetchLoadMatches.forEach(([href, match, fetchMatches], key) => {
|
|
2255
2407
|
// This fetcher was cancelled from a prior action submission - force reload
|
|
2256
2408
|
if (cancelledFetcherLoads.includes(key)) {
|
|
2257
|
-
revalidatingFetchers.push([key, href, match]);
|
|
2409
|
+
revalidatingFetchers.push([key, href, match, fetchMatches]);
|
|
2258
2410
|
} else if (isRevalidationRequired) {
|
|
2259
2411
|
let shouldRevalidate = shouldRevalidateLoader(
|
|
2260
2412
|
href,
|
|
@@ -2266,7 +2418,7 @@ function getMatchesToLoad(
|
|
|
2266
2418
|
actionResult
|
|
2267
2419
|
);
|
|
2268
2420
|
if (shouldRevalidate) {
|
|
2269
|
-
revalidatingFetchers.push([key, href, match]);
|
|
2421
|
+
revalidatingFetchers.push([key, href, match, fetchMatches]);
|
|
2270
2422
|
}
|
|
2271
2423
|
}
|
|
2272
2424
|
});
|
|
@@ -2360,7 +2512,9 @@ async function callLoaderOrAction(
|
|
|
2360
2512
|
type: "loader" | "action",
|
|
2361
2513
|
request: Request,
|
|
2362
2514
|
match: AgnosticDataRouteMatch,
|
|
2363
|
-
|
|
2515
|
+
matches: AgnosticDataRouteMatch[],
|
|
2516
|
+
basename: string | undefined,
|
|
2517
|
+
isStaticRequest: boolean = false,
|
|
2364
2518
|
isRouteRequest: boolean = false
|
|
2365
2519
|
): Promise<DataResult> {
|
|
2366
2520
|
let resultType;
|
|
@@ -2391,23 +2545,46 @@ async function callLoaderOrAction(
|
|
|
2391
2545
|
}
|
|
2392
2546
|
|
|
2393
2547
|
if (result instanceof Response) {
|
|
2394
|
-
// Process redirects
|
|
2395
2548
|
let status = result.status;
|
|
2396
|
-
let location = result.headers.get("Location");
|
|
2397
2549
|
|
|
2398
|
-
//
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2550
|
+
// Process redirects
|
|
2551
|
+
if (status >= 300 && status <= 399) {
|
|
2552
|
+
let location = result.headers.get("Location");
|
|
2553
|
+
invariant(
|
|
2554
|
+
location,
|
|
2555
|
+
"Redirects returned/thrown from loaders/actions must have a Location header"
|
|
2556
|
+
);
|
|
2557
|
+
|
|
2558
|
+
// Support relative routing in redirects
|
|
2559
|
+
let activeMatches = matches.slice(0, matches.indexOf(match) + 1);
|
|
2560
|
+
let routePathnames = getPathContributingMatches(activeMatches).map(
|
|
2561
|
+
(match) => match.pathnameBase
|
|
2562
|
+
);
|
|
2563
|
+
let requestPath = createURL(request.url).pathname;
|
|
2564
|
+
let resolvedLocation = resolveTo(location, routePathnames, requestPath);
|
|
2565
|
+
invariant(
|
|
2566
|
+
createPath(resolvedLocation),
|
|
2567
|
+
`Unable to resolve redirect location: ${result.headers.get("Location")}`
|
|
2568
|
+
);
|
|
2569
|
+
|
|
2570
|
+
// Prepend the basename to the redirect location if we have one
|
|
2571
|
+
if (basename) {
|
|
2572
|
+
let path = resolvedLocation.pathname;
|
|
2573
|
+
resolvedLocation.pathname =
|
|
2574
|
+
path === "/" ? basename : joinPaths([basename, path]);
|
|
2575
|
+
}
|
|
2576
|
+
|
|
2577
|
+
location = createPath(resolvedLocation);
|
|
2403
2578
|
|
|
2404
|
-
|
|
2405
|
-
// Don't process redirects in the router during SSR document requests.
|
|
2579
|
+
// Don't process redirects in the router during static requests requests.
|
|
2406
2580
|
// Instead, throw the Response and let the server handle it with an HTTP
|
|
2407
|
-
// redirect
|
|
2408
|
-
|
|
2581
|
+
// redirect. We also update the Location header in place in this flow so
|
|
2582
|
+
// basename and relative routing is taken into account
|
|
2583
|
+
if (isStaticRequest) {
|
|
2584
|
+
result.headers.set("Location", location);
|
|
2409
2585
|
throw result;
|
|
2410
2586
|
}
|
|
2587
|
+
|
|
2411
2588
|
return {
|
|
2412
2589
|
type: ResultType.redirect,
|
|
2413
2590
|
status,
|
|
@@ -2416,6 +2593,17 @@ async function callLoaderOrAction(
|
|
|
2416
2593
|
};
|
|
2417
2594
|
}
|
|
2418
2595
|
|
|
2596
|
+
// For SSR single-route requests, we want to hand Responses back directly
|
|
2597
|
+
// without unwrapping. We do this with the QueryRouteResponse wrapper
|
|
2598
|
+
// interface so we can know whether it was returned or thrown
|
|
2599
|
+
if (isRouteRequest) {
|
|
2600
|
+
// eslint-disable-next-line no-throw-literal
|
|
2601
|
+
throw {
|
|
2602
|
+
type: resultType || ResultType.data,
|
|
2603
|
+
response: result,
|
|
2604
|
+
};
|
|
2605
|
+
}
|
|
2606
|
+
|
|
2419
2607
|
let data: any;
|
|
2420
2608
|
let contentType = result.headers.get("Content-Type");
|
|
2421
2609
|
if (contentType && contentType.startsWith("application/json")) {
|
|
@@ -2669,16 +2857,18 @@ function findNearestBoundary(
|
|
|
2669
2857
|
);
|
|
2670
2858
|
}
|
|
2671
2859
|
|
|
2672
|
-
function
|
|
2860
|
+
function getShortCircuitMatches(
|
|
2861
|
+
routes: AgnosticDataRouteObject[],
|
|
2862
|
+
status: number,
|
|
2863
|
+
statusText: string
|
|
2864
|
+
): {
|
|
2673
2865
|
matches: AgnosticDataRouteMatch[];
|
|
2674
2866
|
route: AgnosticDataRouteObject;
|
|
2675
2867
|
error: ErrorResponse;
|
|
2676
2868
|
} {
|
|
2677
2869
|
// Prefer a root layout route if present, otherwise shim in a route object
|
|
2678
|
-
let route = routes.find(
|
|
2679
|
-
|
|
2680
|
-
) || {
|
|
2681
|
-
id: "__shim-404-route__",
|
|
2870
|
+
let route = routes.find((r) => r.index || !r.path || r.path === "/") || {
|
|
2871
|
+
id: `__shim-${status}-route__`,
|
|
2682
2872
|
};
|
|
2683
2873
|
|
|
2684
2874
|
return {
|
|
@@ -2691,12 +2881,20 @@ function getNotFoundMatches(routes: AgnosticDataRouteObject[]): {
|
|
|
2691
2881
|
},
|
|
2692
2882
|
],
|
|
2693
2883
|
route,
|
|
2694
|
-
error: new ErrorResponse(
|
|
2884
|
+
error: new ErrorResponse(status, statusText, null),
|
|
2695
2885
|
};
|
|
2696
2886
|
}
|
|
2697
2887
|
|
|
2888
|
+
function getNotFoundMatches(routes: AgnosticDataRouteObject[]) {
|
|
2889
|
+
return getShortCircuitMatches(routes, 404, "Not Found");
|
|
2890
|
+
}
|
|
2891
|
+
|
|
2892
|
+
function getMethodNotAllowedMatches(routes: AgnosticDataRouteObject[]) {
|
|
2893
|
+
return getShortCircuitMatches(routes, 405, "Method Not Allowed");
|
|
2894
|
+
}
|
|
2895
|
+
|
|
2698
2896
|
function getMethodNotAllowedResult(path: Location | string): ErrorResult {
|
|
2699
|
-
let href = typeof path === "string" ? path :
|
|
2897
|
+
let href = typeof path === "string" ? path : createServerHref(path);
|
|
2700
2898
|
console.warn(
|
|
2701
2899
|
"You're trying to submit to a route that does not have an action. To " +
|
|
2702
2900
|
"fix this, please add an `action` function to the route for " +
|
|
@@ -2704,11 +2902,7 @@ function getMethodNotAllowedResult(path: Location | string): ErrorResult {
|
|
|
2704
2902
|
);
|
|
2705
2903
|
return {
|
|
2706
2904
|
type: ResultType.error,
|
|
2707
|
-
error: new ErrorResponse(
|
|
2708
|
-
405,
|
|
2709
|
-
"Method Not Allowed",
|
|
2710
|
-
`No action found for [${href}]`
|
|
2711
|
-
),
|
|
2905
|
+
error: new ErrorResponse(405, "Method Not Allowed", ""),
|
|
2712
2906
|
};
|
|
2713
2907
|
}
|
|
2714
2908
|
|
|
@@ -2723,7 +2917,7 @@ function findRedirect(results: DataResult[]): RedirectResult | undefined {
|
|
|
2723
2917
|
}
|
|
2724
2918
|
|
|
2725
2919
|
// Create an href to represent a "server" URL without the hash
|
|
2726
|
-
function
|
|
2920
|
+
function createServerHref(location: Partial<Path> | Location | URL) {
|
|
2727
2921
|
return (location.pathname || "") + (location.search || "");
|
|
2728
2922
|
}
|
|
2729
2923
|
|
|
@@ -2745,6 +2939,24 @@ function isRedirectResult(result?: DataResult): result is RedirectResult {
|
|
|
2745
2939
|
return (result && result.type) === ResultType.redirect;
|
|
2746
2940
|
}
|
|
2747
2941
|
|
|
2942
|
+
function isRedirectResponse(result: any): result is Response {
|
|
2943
|
+
if (!(result instanceof Response)) {
|
|
2944
|
+
return false;
|
|
2945
|
+
}
|
|
2946
|
+
|
|
2947
|
+
let status = result.status;
|
|
2948
|
+
let location = result.headers.get("Location");
|
|
2949
|
+
return status >= 300 && status <= 399 && location != null;
|
|
2950
|
+
}
|
|
2951
|
+
|
|
2952
|
+
function isQueryRouteResponse(obj: any): obj is QueryRouteResponse {
|
|
2953
|
+
return (
|
|
2954
|
+
obj &&
|
|
2955
|
+
obj.response instanceof Response &&
|
|
2956
|
+
(obj.type === ResultType.data || ResultType.error)
|
|
2957
|
+
);
|
|
2958
|
+
}
|
|
2959
|
+
|
|
2748
2960
|
async function resolveDeferredResults(
|
|
2749
2961
|
currentMatches: AgnosticDataRouteMatch[],
|
|
2750
2962
|
matchesToLoad: AgnosticDataRouteMatch[],
|
|
@@ -2836,11 +3048,15 @@ function getTargetMatch(
|
|
|
2836
3048
|
typeof location === "string" ? parsePath(location).search : location.search;
|
|
2837
3049
|
if (
|
|
2838
3050
|
matches[matches.length - 1].route.index &&
|
|
2839
|
-
|
|
3051
|
+
hasNakedIndexQuery(search || "")
|
|
2840
3052
|
) {
|
|
2841
|
-
|
|
3053
|
+
// Return the leaf index route when index is present
|
|
3054
|
+
return matches[matches.length - 1];
|
|
2842
3055
|
}
|
|
2843
|
-
|
|
3056
|
+
// Otherwise grab the deepest "path contributing" match (ignoring index and
|
|
3057
|
+
// pathless layout routes)
|
|
3058
|
+
let pathMatches = getPathContributingMatches(matches);
|
|
3059
|
+
return pathMatches[pathMatches.length - 1];
|
|
2844
3060
|
}
|
|
2845
3061
|
|
|
2846
3062
|
function createURL(location: Location | string): URL {
|
|
@@ -2848,7 +3064,8 @@ function createURL(location: Location | string): URL {
|
|
|
2848
3064
|
typeof window !== "undefined" && typeof window.location !== "undefined"
|
|
2849
3065
|
? window.location.origin
|
|
2850
3066
|
: "unknown://unknown";
|
|
2851
|
-
let href =
|
|
3067
|
+
let href =
|
|
3068
|
+
typeof location === "string" ? location : createServerHref(location);
|
|
2852
3069
|
return new URL(href, base);
|
|
2853
3070
|
}
|
|
2854
3071
|
//#endregion
|