@remix-run/router 1.3.2 → 1.3.3-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/index.ts CHANGED
@@ -63,7 +63,6 @@ export {
63
63
  createPath,
64
64
  createHashHistory,
65
65
  createMemoryHistory,
66
- invariant,
67
66
  parsePath,
68
67
  } from "./history";
69
68
 
@@ -82,3 +81,5 @@ export {
82
81
  convertRoutesToDataRoutes as UNSAFE_convertRoutesToDataRoutes,
83
82
  getPathContributingMatches as UNSAFE_getPathContributingMatches,
84
83
  } from "./utils";
84
+
85
+ export { invariant as UNSAFE_invariant } from "./history";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remix-run/router",
3
- "version": "1.3.2",
3
+ "version": "1.3.3-pre.1",
4
4
  "description": "Nested/Data-driven/Framework-agnostic Routing",
5
5
  "keywords": [
6
6
  "remix",
package/router.ts CHANGED
@@ -33,6 +33,7 @@ import {
33
33
  joinPaths,
34
34
  matchRoutes,
35
35
  resolveTo,
36
+ stripBasename,
36
37
  warning,
37
38
  } from "./utils";
38
39
 
@@ -211,6 +212,15 @@ export interface Router {
211
212
  */
212
213
  deleteBlocker(key: string): void;
213
214
 
215
+ /**
216
+ * @internal
217
+ * PRIVATE - DO NOT USE
218
+ *
219
+ * HMR needs to pass in-flight route updates to React Router
220
+ * TODO: Replace this with granular route update APIs (addRoute, updateRoute, deleteRoute)
221
+ */
222
+ _internalSetRoutes(routes: AgnosticRouteObject[]): void;
223
+
214
224
  /**
215
225
  * @internal
216
226
  * PRIVATE - DO NOT USE
@@ -556,8 +566,6 @@ interface HandleLoadersResult extends ShortCircuitable {
556
566
  interface FetchLoadMatch {
557
567
  routeId: string;
558
568
  path: string;
559
- match: AgnosticDataRouteMatch;
560
- matches: AgnosticDataRouteMatch[];
561
569
  }
562
570
 
563
571
  /**
@@ -565,6 +573,8 @@ interface FetchLoadMatch {
565
573
  */
566
574
  interface RevalidatingFetcher extends FetchLoadMatch {
567
575
  key: string;
576
+ match: AgnosticDataRouteMatch | null;
577
+ matches: AgnosticDataRouteMatch[] | null;
568
578
  }
569
579
 
570
580
  /**
@@ -644,6 +654,7 @@ export function createRouter(init: RouterInit): Router {
644
654
  );
645
655
 
646
656
  let dataRoutes = convertRoutesToDataRoutes(init.routes);
657
+ let inFlightDataRoutes: AgnosticDataRouteObject[] | undefined;
647
658
  // Cleanup function for history
648
659
  let unlistenHistory: (() => void) | null = null;
649
660
  // Externally-provided functions to call on all state changes
@@ -921,6 +932,11 @@ export function createRouter(init: RouterInit): Router {
921
932
  isMutationMethod(state.navigation.formMethod) &&
922
933
  location.state?._isRedirect !== true);
923
934
 
935
+ if (inFlightDataRoutes) {
936
+ dataRoutes = inFlightDataRoutes;
937
+ inFlightDataRoutes = undefined;
938
+ }
939
+
924
940
  updateState({
925
941
  ...newState, // matches, errors, fetchers go through as-is
926
942
  actionData,
@@ -1108,14 +1124,15 @@ export function createRouter(init: RouterInit): Router {
1108
1124
  saveScrollPosition(state.location, state.matches);
1109
1125
  pendingPreventScrollReset = (opts && opts.preventScrollReset) === true;
1110
1126
 
1127
+ let routesToUse = inFlightDataRoutes || dataRoutes;
1111
1128
  let loadingNavigation = opts && opts.overrideNavigation;
1112
- let matches = matchRoutes(dataRoutes, location, init.basename);
1129
+ let matches = matchRoutes(routesToUse, location, init.basename);
1113
1130
 
1114
1131
  // Short circuit with a 404 on the root error boundary if we match nothing
1115
1132
  if (!matches) {
1116
1133
  let error = getInternalRouterError(404, { pathname: location.pathname });
1117
1134
  let { matches: notFoundMatches, route } =
1118
- getShortCircuitMatches(dataRoutes);
1135
+ getShortCircuitMatches(routesToUse);
1119
1136
  // Cancel all pending deferred on 404s since we don't keep any routes
1120
1137
  cancelActiveDeferreds();
1121
1138
  completeNavigation(location, {
@@ -1352,6 +1369,7 @@ export function createRouter(init: RouterInit): Router {
1352
1369
  }
1353
1370
  : undefined;
1354
1371
 
1372
+ let routesToUse = inFlightDataRoutes || dataRoutes;
1355
1373
  let [matchesToLoad, revalidatingFetchers] = getMatchesToLoad(
1356
1374
  init.history,
1357
1375
  state,
@@ -1361,9 +1379,11 @@ export function createRouter(init: RouterInit): Router {
1361
1379
  isRevalidationRequired,
1362
1380
  cancelledDeferredRoutes,
1363
1381
  cancelledFetcherLoads,
1382
+ fetchLoadMatches,
1383
+ routesToUse,
1384
+ init.basename,
1364
1385
  pendingActionData,
1365
- pendingError,
1366
- fetchLoadMatches
1386
+ pendingError
1367
1387
  );
1368
1388
 
1369
1389
  // Cancel pending deferreds for no-longer-matched routes or routes we're
@@ -1506,7 +1526,8 @@ export function createRouter(init: RouterInit): Router {
1506
1526
 
1507
1527
  if (fetchControllers.has(key)) abortFetcher(key);
1508
1528
 
1509
- let matches = matchRoutes(dataRoutes, href, init.basename);
1529
+ let routesToUse = inFlightDataRoutes || dataRoutes;
1530
+ let matches = matchRoutes(routesToUse, href, init.basename);
1510
1531
  if (!matches) {
1511
1532
  setFetcherError(
1512
1533
  key,
@@ -1528,7 +1549,7 @@ export function createRouter(init: RouterInit): Router {
1528
1549
 
1529
1550
  // Store off the match so we can call it's shouldRevalidate on subsequent
1530
1551
  // revalidations
1531
- fetchLoadMatches.set(key, { routeId, path, match, matches });
1552
+ fetchLoadMatches.set(key, { routeId, path });
1532
1553
  handleFetcherLoader(key, routeId, path, match, matches, submission);
1533
1554
  }
1534
1555
 
@@ -1629,9 +1650,10 @@ export function createRouter(init: RouterInit): Router {
1629
1650
  nextLocation,
1630
1651
  abortController.signal
1631
1652
  );
1653
+ let routesToUse = inFlightDataRoutes || dataRoutes;
1632
1654
  let matches =
1633
1655
  state.navigation.state !== "idle"
1634
- ? matchRoutes(dataRoutes, state.navigation.location, init.basename)
1656
+ ? matchRoutes(routesToUse, state.navigation.location, init.basename)
1635
1657
  : state.matches;
1636
1658
 
1637
1659
  invariant(matches, "Didn't find any matches after fetcher action");
@@ -1656,9 +1678,11 @@ export function createRouter(init: RouterInit): Router {
1656
1678
  isRevalidationRequired,
1657
1679
  cancelledDeferredRoutes,
1658
1680
  cancelledFetcherLoads,
1681
+ fetchLoadMatches,
1682
+ routesToUse,
1683
+ init.basename,
1659
1684
  { [match.route.id]: actionResult.data },
1660
- undefined, // No need to send through errors since we short circuit above
1661
- fetchLoadMatches
1685
+ undefined // No need to send through errors since we short circuit above
1662
1686
  );
1663
1687
 
1664
1688
  // Put all revalidating fetchers into the loading state, except for the
@@ -1912,15 +1936,17 @@ export function createRouter(init: RouterInit): Router {
1912
1936
  redirectLocation,
1913
1937
  "Expected a location on the redirect navigation"
1914
1938
  );
1915
-
1916
1939
  // Check if this an absolute external redirect that goes to a new origin
1917
1940
  if (
1918
1941
  ABSOLUTE_URL_REGEX.test(redirect.location) &&
1919
1942
  isBrowser &&
1920
1943
  typeof window?.location !== "undefined"
1921
1944
  ) {
1922
- let newOrigin = init.history.createURL(redirect.location).origin;
1923
- if (window.location.origin !== newOrigin) {
1945
+ let url = init.history.createURL(redirect.location);
1946
+ let isDifferentBasename =
1947
+ stripBasename(url.pathname, init.basename || "/") == null;
1948
+
1949
+ if (window.location.origin !== url.origin || isDifferentBasename) {
1924
1950
  if (replace) {
1925
1951
  window.location.replace(redirect.location);
1926
1952
  } else {
@@ -1997,15 +2023,23 @@ export function createRouter(init: RouterInit): Router {
1997
2023
  ...matchesToLoad.map((match) =>
1998
2024
  callLoaderOrAction("loader", request, match, matches, router.basename)
1999
2025
  ),
2000
- ...fetchersToLoad.map((f) =>
2001
- callLoaderOrAction(
2002
- "loader",
2003
- createClientSideRequest(init.history, f.path, request.signal),
2004
- f.match,
2005
- f.matches,
2006
- router.basename
2007
- )
2008
- ),
2026
+ ...fetchersToLoad.map((f) => {
2027
+ if (f.matches && f.match) {
2028
+ return callLoaderOrAction(
2029
+ "loader",
2030
+ createClientSideRequest(init.history, f.path, request.signal),
2031
+ f.match,
2032
+ f.matches,
2033
+ router.basename
2034
+ );
2035
+ } else {
2036
+ let error: ErrorResult = {
2037
+ type: ResultType.error,
2038
+ error: getInternalRouterError(404, { pathname: f.path }),
2039
+ };
2040
+ return error;
2041
+ }
2042
+ }),
2009
2043
  ]);
2010
2044
  let loaderResults = results.slice(0, matchesToLoad.length);
2011
2045
  let fetcherResults = results.slice(matchesToLoad.length);
@@ -2266,6 +2300,10 @@ export function createRouter(init: RouterInit): Router {
2266
2300
  return null;
2267
2301
  }
2268
2302
 
2303
+ function _internalSetRoutes(newRoutes: AgnosticDataRouteObject[]) {
2304
+ inFlightDataRoutes = newRoutes;
2305
+ }
2306
+
2269
2307
  router = {
2270
2308
  get basename() {
2271
2309
  return init.basename;
@@ -2293,6 +2331,9 @@ export function createRouter(init: RouterInit): Router {
2293
2331
  deleteBlocker,
2294
2332
  _internalFetchControllers: fetchControllers,
2295
2333
  _internalActiveDeferreds: activeDeferreds,
2334
+ // TODO: Remove setRoutes, it's temporary to avoid dealing with
2335
+ // updating the tree while validating the update algorithm.
2336
+ _internalSetRoutes,
2296
2337
  };
2297
2338
 
2298
2339
  return router;
@@ -2891,9 +2932,11 @@ function getMatchesToLoad(
2891
2932
  isRevalidationRequired: boolean,
2892
2933
  cancelledDeferredRoutes: string[],
2893
2934
  cancelledFetcherLoads: string[],
2935
+ fetchLoadMatches: Map<string, FetchLoadMatch>,
2936
+ routesToUse: AgnosticDataRouteObject[],
2937
+ basename: string | undefined,
2894
2938
  pendingActionData?: RouteData,
2895
- pendingError?: RouteData,
2896
- fetchLoadMatches?: Map<string, FetchLoadMatch>
2939
+ pendingError?: RouteData
2897
2940
  ): [AgnosticDataRouteMatch[], RevalidatingFetcher[]] {
2898
2941
  let actionResult = pendingError
2899
2942
  ? Object.values(pendingError)[0]
@@ -2951,34 +2994,55 @@ function getMatchesToLoad(
2951
2994
 
2952
2995
  // Pick fetcher.loads that need to be revalidated
2953
2996
  let revalidatingFetchers: RevalidatingFetcher[] = [];
2954
- fetchLoadMatches &&
2955
- fetchLoadMatches.forEach((f, key) => {
2956
- if (!matches.some((m) => m.route.id === f.routeId)) {
2957
- // This fetcher is not going to be present in the subsequent render so
2958
- // there's no need to revalidate it
2959
- return;
2960
- } else if (cancelledFetcherLoads.includes(key)) {
2961
- // This fetcher was cancelled from a prior action submission - force reload
2962
- revalidatingFetchers.push({ key, ...f });
2963
- } else {
2964
- // Revalidating fetchers are decoupled from the route matches since they
2965
- // hit a static href, so they _always_ check shouldRevalidate and the
2966
- // default is strictly if a revalidation is explicitly required (action
2967
- // submissions, useRevalidator, X-Remix-Revalidate).
2968
- let shouldRevalidate = shouldRevalidateLoader(f.match, {
2969
- currentUrl,
2970
- currentParams: state.matches[state.matches.length - 1].params,
2971
- nextUrl,
2972
- nextParams: matches[matches.length - 1].params,
2973
- ...submission,
2974
- actionResult,
2975
- defaultShouldRevalidate,
2976
- });
2977
- if (shouldRevalidate) {
2978
- revalidatingFetchers.push({ key, ...f });
2979
- }
2980
- }
2997
+ fetchLoadMatches.forEach((f, key) => {
2998
+ // Don't revalidate if fetcher won't be present in the subsequent render
2999
+ if (!matches.some((m) => m.route.id === f.routeId)) {
3000
+ return;
3001
+ }
3002
+
3003
+ let fetcherMatches = matchRoutes(routesToUse, f.path, basename);
3004
+
3005
+ // If the fetcher path no longer matches, push it in with null matches so
3006
+ // we can trigger a 404 in callLoadersAndMaybeResolveData
3007
+ if (!fetcherMatches) {
3008
+ revalidatingFetchers.push({ key, ...f, matches: null, match: null });
3009
+ return;
3010
+ }
3011
+
3012
+ let fetcherMatch = getTargetMatch(fetcherMatches, f.path);
3013
+
3014
+ if (cancelledFetcherLoads.includes(key)) {
3015
+ revalidatingFetchers.push({
3016
+ key,
3017
+ matches: fetcherMatches,
3018
+ match: fetcherMatch,
3019
+ ...f,
3020
+ });
3021
+ return;
3022
+ }
3023
+
3024
+ // Revalidating fetchers are decoupled from the route matches since they
3025
+ // hit a static href, so they _always_ check shouldRevalidate and the
3026
+ // default is strictly if a revalidation is explicitly required (action
3027
+ // submissions, useRevalidator, X-Remix-Revalidate).
3028
+ let shouldRevalidate = shouldRevalidateLoader(fetcherMatch, {
3029
+ currentUrl,
3030
+ currentParams: state.matches[state.matches.length - 1].params,
3031
+ nextUrl,
3032
+ nextParams: matches[matches.length - 1].params,
3033
+ ...submission,
3034
+ actionResult,
3035
+ defaultShouldRevalidate,
2981
3036
  });
3037
+ if (shouldRevalidate) {
3038
+ revalidatingFetchers.push({
3039
+ key,
3040
+ matches: fetcherMatches,
3041
+ match: fetcherMatch,
3042
+ ...f,
3043
+ });
3044
+ }
3045
+ });
2982
3046
 
2983
3047
  return [navigationMatches, revalidatingFetchers];
2984
3048
  }
@@ -3112,14 +3176,15 @@ async function callLoaderOrAction(
3112
3176
 
3113
3177
  location = createPath(resolvedLocation);
3114
3178
  } else if (!isStaticRequest) {
3115
- // Strip off the protocol+origin for same-origin absolute redirects.
3116
- // If this is a static reques, we can let it go back to the browser
3117
- // as-is
3179
+ // Strip off the protocol+origin for same-origin + same-basename absolute
3180
+ // redirects. If this is a static request, we can let it go back to the
3181
+ // browser as-is
3118
3182
  let currentUrl = new URL(request.url);
3119
3183
  let url = location.startsWith("//")
3120
3184
  ? new URL(currentUrl.protocol + location)
3121
3185
  : new URL(location);
3122
- if (url.origin === currentUrl.origin) {
3186
+ let isSameBasename = stripBasename(url.pathname, basename) != null;
3187
+ if (url.origin === currentUrl.origin && isSameBasename) {
3123
3188
  location = url.pathname + url.search + url.hash;
3124
3189
  }
3125
3190
  }
@@ -3183,7 +3248,12 @@ async function callLoaderOrAction(
3183
3248
  }
3184
3249
 
3185
3250
  if (result instanceof DeferredData) {
3186
- return { type: ResultType.deferred, deferredData: result };
3251
+ return {
3252
+ type: ResultType.deferred,
3253
+ deferredData: result,
3254
+ statusCode: result.init?.status,
3255
+ headers: result.init?.headers && new Headers(result.init.headers),
3256
+ };
3187
3257
  }
3188
3258
 
3189
3259
  return { type: ResultType.data, data: result };
@@ -3356,7 +3426,7 @@ function processLoaderData(
3356
3426
 
3357
3427
  // Process fetcher non-redirect errors
3358
3428
  if (isErrorResult(result)) {
3359
- let boundaryMatch = findNearestBoundary(state.matches, match.route.id);
3429
+ let boundaryMatch = findNearestBoundary(state.matches, match?.route.id);
3360
3430
  if (!(errors && errors[boundaryMatch.route.id])) {
3361
3431
  errors = {
3362
3432
  ...errors,
@@ -3406,7 +3476,9 @@ function mergeLoaderData(
3406
3476
  // incoming object with an undefined value, which is how we unset a prior
3407
3477
  // loaderData if we encounter a loader error
3408
3478
  }
3409
- } else if (loaderData[id] !== undefined) {
3479
+ } else if (loaderData[id] !== undefined && match.route.loader) {
3480
+ // Preserve existing keys not included in newLoaderData and where a loader
3481
+ // wasn't removed by HMR
3410
3482
  mergedLoaderData[id] = loaderData[id];
3411
3483
  }
3412
3484
 
@@ -3580,7 +3652,7 @@ function isMutationMethod(method?: string): method is MutationFormMethod {
3580
3652
 
3581
3653
  async function resolveDeferredResults(
3582
3654
  currentMatches: AgnosticDataRouteMatch[],
3583
- matchesToLoad: AgnosticDataRouteMatch[],
3655
+ matchesToLoad: (AgnosticDataRouteMatch | null)[],
3584
3656
  results: DataResult[],
3585
3657
  signal: AbortSignal,
3586
3658
  isFetcher: boolean,
@@ -3589,8 +3661,15 @@ async function resolveDeferredResults(
3589
3661
  for (let index = 0; index < results.length; index++) {
3590
3662
  let result = results[index];
3591
3663
  let match = matchesToLoad[index];
3664
+ // If we don't have a match, then we can have a deferred result to do
3665
+ // anything with. This is for revalidating fetchers where the route was
3666
+ // removed during HMR
3667
+ if (!match) {
3668
+ continue;
3669
+ }
3670
+
3592
3671
  let currentMatch = currentMatches.find(
3593
- (m) => m.route.id === match.route.id
3672
+ (m) => m.route.id === match!.route.id
3594
3673
  );
3595
3674
  let isRevalidatingLoader =
3596
3675
  currentMatch != null &&