@remix-run/router 1.13.1 → 1.14.0-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/dist/utils.d.ts CHANGED
@@ -135,9 +135,11 @@ type DataFunctionValue = Response | NonNullable<unknown> | null;
135
135
  /**
136
136
  * Route loader function signature
137
137
  */
138
- export interface LoaderFunction<Context = any> {
138
+ export type LoaderFunction<Context = any> = {
139
139
  (args: LoaderFunctionArgs<Context>): Promise<DataFunctionValue> | DataFunctionValue;
140
- }
140
+ } & {
141
+ hydrate?: boolean;
142
+ };
141
143
  /**
142
144
  * Route action function signature
143
145
  */
@@ -399,6 +401,7 @@ export declare function resolvePath(to: To, fromPathname?: string): Path;
399
401
  * </Route>
400
402
  */
401
403
  export declare function getPathContributingMatches<T extends AgnosticRouteMatch = AgnosticRouteMatch>(matches: T[]): T[];
404
+ export declare function getResolveToMatches<T extends AgnosticRouteMatch = AgnosticRouteMatch>(matches: T[], v7_relativeSplatPath: boolean): string[];
402
405
  /**
403
406
  * @private
404
407
  */
package/index.ts CHANGED
@@ -87,7 +87,7 @@ export {
87
87
  ErrorResponseImpl as UNSAFE_ErrorResponseImpl,
88
88
  convertRoutesToDataRoutes as UNSAFE_convertRoutesToDataRoutes,
89
89
  convertRouteMatchToUiMatch as UNSAFE_convertRouteMatchToUiMatch,
90
- getPathContributingMatches as UNSAFE_getPathContributingMatches,
90
+ getResolveToMatches as UNSAFE_getResolveToMatches,
91
91
  } from "./utils";
92
92
 
93
93
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remix-run/router",
3
- "version": "1.13.1",
3
+ "version": "1.14.0-pre.0",
4
4
  "description": "Nested/Data-driven/Framework-agnostic Routing",
5
5
  "keywords": [
6
6
  "remix",
package/router.ts CHANGED
@@ -40,6 +40,7 @@ import {
40
40
  convertRouteMatchToUiMatch,
41
41
  convertRoutesToDataRoutes,
42
42
  getPathContributingMatches,
43
+ getResolveToMatches,
43
44
  immutableRouteKeys,
44
45
  isRouteErrorResponse,
45
46
  joinPaths,
@@ -64,6 +65,14 @@ export interface Router {
64
65
  */
65
66
  get basename(): RouterInit["basename"];
66
67
 
68
+ /**
69
+ * @internal
70
+ * PRIVATE - DO NOT USE
71
+ *
72
+ * Return the future config for the router
73
+ */
74
+ get future(): FutureConfig;
75
+
67
76
  /**
68
77
  * @internal
69
78
  * PRIVATE - DO NOT USE
@@ -345,7 +354,9 @@ export type HydrationState = Partial<
345
354
  export interface FutureConfig {
346
355
  v7_fetcherPersist: boolean;
347
356
  v7_normalizeFormMethod: boolean;
357
+ v7_partialHydration: boolean;
348
358
  v7_prependBasename: boolean;
359
+ v7_relativeSplatPath: boolean;
349
360
  }
350
361
 
351
362
  /**
@@ -769,7 +780,9 @@ export function createRouter(init: RouterInit): Router {
769
780
  let future: FutureConfig = {
770
781
  v7_fetcherPersist: false,
771
782
  v7_normalizeFormMethod: false,
783
+ v7_partialHydration: false,
772
784
  v7_prependBasename: false,
785
+ v7_relativeSplatPath: false,
773
786
  ...init.future,
774
787
  };
775
788
  // Cleanup function for history
@@ -804,12 +817,19 @@ export function createRouter(init: RouterInit): Router {
804
817
  initialErrors = { [route.id]: error };
805
818
  }
806
819
 
820
+ // "Initialized" here really means "Can `RouterProvider` render my route tree?"
821
+ // Prior to `route.HydrateFallback`, we only had a root `fallbackElement` so we used
822
+ // `state.initialized` to render that instead of `<DataRoutes>`. Now that we
823
+ // support route level fallbacks we can always render and we'll just render
824
+ // as deep as we have data for and detect the nearest ancestor HydrateFallback
807
825
  let initialized =
826
+ future.v7_partialHydration ||
808
827
  // All initialMatches need to be loaded before we're ready. If we have lazy
809
828
  // functions around still then we'll need to run them in initialize()
810
- !initialMatches.some((m) => m.route.lazy) &&
811
- // And we have to either have no loaders or have been provided hydrationData
812
- (!initialMatches.some((m) => m.route.loader) || init.hydrationData != null);
829
+ (!initialMatches.some((m) => m.route.lazy) &&
830
+ // And we have to either have no loaders or have been provided hydrationData
831
+ (!initialMatches.some((m) => m.route.loader) ||
832
+ init.hydrationData != null));
813
833
 
814
834
  let router: Router;
815
835
  let state: RouterState = {
@@ -990,8 +1010,14 @@ export function createRouter(init: RouterInit): Router {
990
1010
  // in the normal navigation flow. For SSR it's expected that lazy modules are
991
1011
  // resolved prior to router creation since we can't go into a fallbackElement
992
1012
  // UI for SSR'd apps
993
- if (!state.initialized) {
994
- startNavigation(HistoryAction.Pop, state.location);
1013
+ if (
1014
+ !state.initialized ||
1015
+ (future.v7_partialHydration &&
1016
+ state.matches.some((m) => isUnhydratedRoute(state, m.route)))
1017
+ ) {
1018
+ startNavigation(HistoryAction.Pop, state.location, {
1019
+ initialHydration: true,
1020
+ });
995
1021
  }
996
1022
 
997
1023
  return router;
@@ -1231,6 +1257,7 @@ export function createRouter(init: RouterInit): Router {
1231
1257
  basename,
1232
1258
  future.v7_prependBasename,
1233
1259
  to,
1260
+ future.v7_relativeSplatPath,
1234
1261
  opts?.fromRouteId,
1235
1262
  opts?.relative
1236
1263
  );
@@ -1363,6 +1390,7 @@ export function createRouter(init: RouterInit): Router {
1363
1390
  historyAction: HistoryAction,
1364
1391
  location: Location,
1365
1392
  opts?: {
1393
+ initialHydration?: boolean;
1366
1394
  submission?: Submission;
1367
1395
  fetcherSubmission?: Submission;
1368
1396
  overrideNavigation?: Navigation;
@@ -1487,6 +1515,7 @@ export function createRouter(init: RouterInit): Router {
1487
1515
  opts && opts.submission,
1488
1516
  opts && opts.fetcherSubmission,
1489
1517
  opts && opts.replace,
1518
+ opts && opts.initialHydration === true,
1490
1519
  flushSync,
1491
1520
  pendingActionData,
1492
1521
  pendingError
@@ -1545,7 +1574,8 @@ export function createRouter(init: RouterInit): Router {
1545
1574
  matches,
1546
1575
  manifest,
1547
1576
  mapRouteProperties,
1548
- basename
1577
+ basename,
1578
+ future.v7_relativeSplatPath
1549
1579
  );
1550
1580
 
1551
1581
  if (request.signal.aborted) {
@@ -1607,6 +1637,7 @@ export function createRouter(init: RouterInit): Router {
1607
1637
  submission?: Submission,
1608
1638
  fetcherSubmission?: Submission,
1609
1639
  replace?: boolean,
1640
+ initialHydration?: boolean,
1610
1641
  flushSync?: boolean,
1611
1642
  pendingActionData?: RouteData,
1612
1643
  pendingError?: RouteData
@@ -1629,6 +1660,7 @@ export function createRouter(init: RouterInit): Router {
1629
1660
  matches,
1630
1661
  activeSubmission,
1631
1662
  location,
1663
+ future.v7_partialHydration && initialHydration === true,
1632
1664
  isRevalidationRequired,
1633
1665
  cancelledDeferredRoutes,
1634
1666
  cancelledFetcherLoads,
@@ -1674,7 +1706,12 @@ export function createRouter(init: RouterInit): Router {
1674
1706
  // state. If not, we need to switch to our loading state and load data,
1675
1707
  // preserving any new action data or existing action data (in the case of
1676
1708
  // a revalidation interrupting an actionReload)
1677
- if (!isUninterruptedRevalidation) {
1709
+ // If we have partialHydration enabled, then don't update the state for the
1710
+ // initial data load since iot's not a "navigation"
1711
+ if (
1712
+ !isUninterruptedRevalidation &&
1713
+ (!future.v7_partialHydration || !initialHydration)
1714
+ ) {
1678
1715
  revalidatingFetchers.forEach((rf) => {
1679
1716
  let fetcher = state.fetchers.get(rf.key);
1680
1717
  let revalidatingFetcher = getLoadingFetcher(
@@ -1824,6 +1861,7 @@ export function createRouter(init: RouterInit): Router {
1824
1861
  basename,
1825
1862
  future.v7_prependBasename,
1826
1863
  href,
1864
+ future.v7_relativeSplatPath,
1827
1865
  routeId,
1828
1866
  opts?.relative
1829
1867
  );
@@ -1930,7 +1968,8 @@ export function createRouter(init: RouterInit): Router {
1930
1968
  requestMatches,
1931
1969
  manifest,
1932
1970
  mapRouteProperties,
1933
- basename
1971
+ basename,
1972
+ future.v7_relativeSplatPath
1934
1973
  );
1935
1974
 
1936
1975
  if (fetchRequest.signal.aborted) {
@@ -2003,6 +2042,7 @@ export function createRouter(init: RouterInit): Router {
2003
2042
  matches,
2004
2043
  submission,
2005
2044
  nextLocation,
2045
+ false,
2006
2046
  isRevalidationRequired,
2007
2047
  cancelledDeferredRoutes,
2008
2048
  cancelledFetcherLoads,
@@ -2173,7 +2213,8 @@ export function createRouter(init: RouterInit): Router {
2173
2213
  matches,
2174
2214
  manifest,
2175
2215
  mapRouteProperties,
2176
- basename
2216
+ basename,
2217
+ future.v7_relativeSplatPath
2177
2218
  );
2178
2219
 
2179
2220
  // Deferred isn't supported for fetcher loads, await everything and treat it
@@ -2369,7 +2410,8 @@ export function createRouter(init: RouterInit): Router {
2369
2410
  matches,
2370
2411
  manifest,
2371
2412
  mapRouteProperties,
2372
- basename
2413
+ basename,
2414
+ future.v7_relativeSplatPath
2373
2415
  )
2374
2416
  ),
2375
2417
  ...fetchersToLoad.map((f) => {
@@ -2381,7 +2423,8 @@ export function createRouter(init: RouterInit): Router {
2381
2423
  f.matches,
2382
2424
  manifest,
2383
2425
  mapRouteProperties,
2384
- basename
2426
+ basename,
2427
+ future.v7_relativeSplatPath
2385
2428
  );
2386
2429
  } else {
2387
2430
  let error: ErrorResult = {
@@ -2723,6 +2766,9 @@ export function createRouter(init: RouterInit): Router {
2723
2766
  get basename() {
2724
2767
  return basename;
2725
2768
  },
2769
+ get future() {
2770
+ return future;
2771
+ },
2726
2772
  get state() {
2727
2773
  return state;
2728
2774
  },
@@ -2764,6 +2810,13 @@ export function createRouter(init: RouterInit): Router {
2764
2810
 
2765
2811
  export const UNSAFE_DEFERRED_SYMBOL = Symbol("deferred");
2766
2812
 
2813
+ /**
2814
+ * Future flags to toggle new feature behavior
2815
+ */
2816
+ export interface StaticHandlerFutureConfig {
2817
+ v7_relativeSplatPath: boolean;
2818
+ }
2819
+
2767
2820
  export interface CreateStaticHandlerOptions {
2768
2821
  basename?: string;
2769
2822
  /**
@@ -2771,6 +2824,7 @@ export interface CreateStaticHandlerOptions {
2771
2824
  */
2772
2825
  detectErrorBoundary?: DetectErrorBoundaryFunction;
2773
2826
  mapRouteProperties?: MapRoutePropertiesFunction;
2827
+ future?: Partial<StaticHandlerFutureConfig>;
2774
2828
  }
2775
2829
 
2776
2830
  export function createStaticHandler(
@@ -2796,6 +2850,11 @@ export function createStaticHandler(
2796
2850
  } else {
2797
2851
  mapRouteProperties = defaultMapRouteProperties;
2798
2852
  }
2853
+ // Config driven behavior flags
2854
+ let future: StaticHandlerFutureConfig = {
2855
+ v7_relativeSplatPath: false,
2856
+ ...(opts ? opts.future : null),
2857
+ };
2799
2858
 
2800
2859
  let dataRoutes = convertRoutesToDataRoutes(
2801
2860
  routes,
@@ -3058,6 +3117,7 @@ export function createStaticHandler(
3058
3117
  manifest,
3059
3118
  mapRouteProperties,
3060
3119
  basename,
3120
+ future.v7_relativeSplatPath,
3061
3121
  { isStaticRequest: true, isRouteRequest, requestContext }
3062
3122
  );
3063
3123
 
@@ -3226,6 +3286,7 @@ export function createStaticHandler(
3226
3286
  manifest,
3227
3287
  mapRouteProperties,
3228
3288
  basename,
3289
+ future.v7_relativeSplatPath,
3229
3290
  { isStaticRequest: true, isRouteRequest, requestContext }
3230
3291
  )
3231
3292
  ),
@@ -3316,6 +3377,7 @@ function normalizeTo(
3316
3377
  basename: string,
3317
3378
  prependBasename: boolean,
3318
3379
  to: To | null,
3380
+ v7_relativeSplatPath: boolean,
3319
3381
  fromRouteId?: string,
3320
3382
  relative?: RelativeRoutingType
3321
3383
  ) {
@@ -3340,7 +3402,7 @@ function normalizeTo(
3340
3402
  // Resolve the relative path
3341
3403
  let path = resolveTo(
3342
3404
  to ? to : ".",
3343
- getPathContributingMatches(contextualMatches).map((m) => m.pathnameBase),
3405
+ getResolveToMatches(contextualMatches, v7_relativeSplatPath),
3344
3406
  stripBasename(location.pathname, basename) || location.pathname,
3345
3407
  relative === "path"
3346
3408
  );
@@ -3548,6 +3610,7 @@ function getMatchesToLoad(
3548
3610
  matches: AgnosticDataRouteMatch[],
3549
3611
  submission: Submission | undefined,
3550
3612
  location: Location,
3613
+ isInitialLoad: boolean,
3551
3614
  isRevalidationRequired: boolean,
3552
3615
  cancelledDeferredRoutes: string[],
3553
3616
  cancelledFetcherLoads: string[],
@@ -3573,10 +3636,17 @@ function getMatchesToLoad(
3573
3636
  let boundaryMatches = getLoaderMatchesUntilBoundary(matches, boundaryId);
3574
3637
 
3575
3638
  let navigationMatches = boundaryMatches.filter((match, index) => {
3639
+ if (isInitialLoad) {
3640
+ // On initial hydration we don't do any shouldRevalidate stuff - we just
3641
+ // call the unhydrated loaders
3642
+ return isUnhydratedRoute(state, match.route);
3643
+ }
3644
+
3576
3645
  if (match.route.lazy) {
3577
3646
  // We haven't loaded this route yet so we don't know if it's got a loader!
3578
3647
  return true;
3579
3648
  }
3649
+
3580
3650
  if (match.route.loader == null) {
3581
3651
  return false;
3582
3652
  }
@@ -3618,8 +3688,13 @@ function getMatchesToLoad(
3618
3688
  // Pick fetcher.loads that need to be revalidated
3619
3689
  let revalidatingFetchers: RevalidatingFetcher[] = [];
3620
3690
  fetchLoadMatches.forEach((f, key) => {
3621
- // Don't revalidate if fetcher won't be present in the subsequent render
3691
+ // Don't revalidate:
3692
+ // - on initial load (shouldn't be any fetchers then anyway)
3693
+ // - if fetcher won't be present in the subsequent render
3694
+ // - no longer matches the URL (v7_fetcherPersist=false)
3695
+ // - was unmounted but persisted due to v7_fetcherPersist=true
3622
3696
  if (
3697
+ isInitialLoad ||
3623
3698
  !matches.some((m) => m.route.id === f.routeId) ||
3624
3699
  deletedFetchers.has(key)
3625
3700
  ) {
@@ -3695,6 +3770,23 @@ function getMatchesToLoad(
3695
3770
  return [navigationMatches, revalidatingFetchers];
3696
3771
  }
3697
3772
 
3773
+ // Is this route unhydrated (when v7_partialHydration=true) such that we need
3774
+ // to call it's loader on the initial router creation
3775
+ function isUnhydratedRoute(state: RouterState, route: AgnosticDataRouteObject) {
3776
+ if (!route.loader) {
3777
+ return false;
3778
+ }
3779
+ if (route.loader.hydrate) {
3780
+ return true;
3781
+ }
3782
+ return (
3783
+ state.loaderData[route.id] === undefined &&
3784
+ (!state.errors ||
3785
+ // Loader ran but errored - don't re-run
3786
+ state.errors[route.id] === undefined)
3787
+ );
3788
+ }
3789
+
3698
3790
  function isNewLoader(
3699
3791
  currentLoaderData: RouteData,
3700
3792
  currentMatch: AgnosticDataRouteMatch,
@@ -3830,6 +3922,7 @@ async function callLoaderOrAction(
3830
3922
  manifest: RouteManifest,
3831
3923
  mapRouteProperties: MapRoutePropertiesFunction,
3832
3924
  basename: string,
3925
+ v7_relativeSplatPath: boolean,
3833
3926
  opts: {
3834
3927
  isStaticRequest?: boolean;
3835
3928
  isRouteRequest?: boolean;
@@ -3943,7 +4036,8 @@ async function callLoaderOrAction(
3943
4036
  matches.slice(0, matches.indexOf(match) + 1),
3944
4037
  basename,
3945
4038
  true,
3946
- location
4039
+ location,
4040
+ v7_relativeSplatPath
3947
4041
  );
3948
4042
  } else if (!opts.isStaticRequest) {
3949
4043
  // Strip off the protocol+origin for same-origin + same-basename absolute
@@ -3990,13 +4084,18 @@ async function callLoaderOrAction(
3990
4084
  }
3991
4085
 
3992
4086
  let data: any;
3993
- let contentType = result.headers.get("Content-Type");
3994
- // Check between word boundaries instead of startsWith() due to the last
3995
- // paragraph of https://httpwg.org/specs/rfc9110.html#field.content-type
3996
- if (contentType && /\bapplication\/json\b/.test(contentType)) {
3997
- data = await result.json();
3998
- } else {
3999
- data = await result.text();
4087
+
4088
+ try {
4089
+ let contentType = result.headers.get("Content-Type");
4090
+ // Check between word boundaries instead of startsWith() due to the last
4091
+ // paragraph of https://httpwg.org/specs/rfc9110.html#field.content-type
4092
+ if (contentType && /\bapplication\/json\b/.test(contentType)) {
4093
+ data = await result.json();
4094
+ } else {
4095
+ data = await result.text();
4096
+ }
4097
+ } catch (e) {
4098
+ return { type: ResultType.error, error: e };
4000
4099
  }
4001
4100
 
4002
4101
  if (resultType === ResultType.error) {
package/utils.ts CHANGED
@@ -169,11 +169,11 @@ type DataFunctionValue = Response | NonNullable<unknown> | null;
169
169
  /**
170
170
  * Route loader function signature
171
171
  */
172
- export interface LoaderFunction<Context = any> {
172
+ export type LoaderFunction<Context = any> = {
173
173
  (args: LoaderFunctionArgs<Context>):
174
174
  | Promise<DataFunctionValue>
175
175
  | DataFunctionValue;
176
- }
176
+ } & { hydrate?: boolean };
177
177
 
178
178
  /**
179
179
  * Route action function signature
@@ -1145,6 +1145,25 @@ export function getPathContributingMatches<
1145
1145
  );
1146
1146
  }
1147
1147
 
1148
+ // Return the array of pathnames for the current route matches - used to
1149
+ // generate the routePathnames input for resolveTo()
1150
+ export function getResolveToMatches<
1151
+ T extends AgnosticRouteMatch = AgnosticRouteMatch
1152
+ >(matches: T[], v7_relativeSplatPath: boolean) {
1153
+ let pathMatches = getPathContributingMatches(matches);
1154
+
1155
+ // When v7_relativeSplatPath is enabled, use the full pathname for the leaf
1156
+ // match so we include splat values for "." links. See:
1157
+ // https://github.com/remix-run/react-router/issues/11052#issuecomment-1836589329
1158
+ if (v7_relativeSplatPath) {
1159
+ return pathMatches.map((match, idx) =>
1160
+ idx === matches.length - 1 ? match.pathname : match.pathnameBase
1161
+ );
1162
+ }
1163
+
1164
+ return pathMatches.map((match) => match.pathnameBase);
1165
+ }
1166
+
1148
1167
  /**
1149
1168
  * @private
1150
1169
  */
@@ -1191,9 +1210,12 @@ export function resolveTo(
1191
1210
  if (toPathname == null) {
1192
1211
  from = locationPathname;
1193
1212
  } else if (isPathRelative) {
1194
- let fromSegments = routePathnames[routePathnames.length - 1]
1195
- .replace(/^\//, "")
1196
- .split("/");
1213
+ let fromSegments =
1214
+ routePathnames.length === 0
1215
+ ? []
1216
+ : routePathnames[routePathnames.length - 1]
1217
+ .replace(/^\//, "")
1218
+ .split("/");
1197
1219
 
1198
1220
  if (toPathname.startsWith("..")) {
1199
1221
  let toSegments = toPathname.split("/");