@remix-run/router 1.13.1 → 1.14.0-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/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.1",
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,34 @@ export function createRouter(init: RouterInit): Router {
804
817
  initialErrors = { [route.id]: error };
805
818
  }
806
819
 
807
- let initialized =
820
+ let initialized: boolean;
821
+ let hasLazyRoutes = initialMatches.some((m) => m.route.lazy);
822
+ let hasLoaders = initialMatches.some((m) => m.route.loader);
823
+ if (hasLazyRoutes) {
808
824
  // All initialMatches need to be loaded before we're ready. If we have lazy
809
825
  // 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);
826
+ initialized = false;
827
+ } else if (!hasLoaders) {
828
+ // If we've got no loaders to run, then we're good to go
829
+ initialized = true;
830
+ } else if (future.v7_partialHydration) {
831
+ // If partial hydration is enabled, we're initialized so long as we were
832
+ // provided with hydrationData for every route with a loader, and no loaders
833
+ // were marked for explicit hydration
834
+ let loaderData = init.hydrationData ? init.hydrationData.loaderData : null;
835
+ let errors = init.hydrationData ? init.hydrationData.errors : null;
836
+ initialized = initialMatches.every(
837
+ (m) =>
838
+ m.route.loader &&
839
+ m.route.loader.hydrate !== true &&
840
+ ((loaderData && loaderData[m.route.id] !== undefined) ||
841
+ (errors && errors[m.route.id] !== undefined))
842
+ );
843
+ } else {
844
+ // Without partial hydration - we're initialized if we were provided any
845
+ // hydrationData - which is expected to be complete
846
+ initialized = init.hydrationData != null;
847
+ }
813
848
 
814
849
  let router: Router;
815
850
  let state: RouterState = {
@@ -991,7 +1026,9 @@ export function createRouter(init: RouterInit): Router {
991
1026
  // resolved prior to router creation since we can't go into a fallbackElement
992
1027
  // UI for SSR'd apps
993
1028
  if (!state.initialized) {
994
- startNavigation(HistoryAction.Pop, state.location);
1029
+ startNavigation(HistoryAction.Pop, state.location, {
1030
+ initialHydration: true,
1031
+ });
995
1032
  }
996
1033
 
997
1034
  return router;
@@ -1231,6 +1268,7 @@ export function createRouter(init: RouterInit): Router {
1231
1268
  basename,
1232
1269
  future.v7_prependBasename,
1233
1270
  to,
1271
+ future.v7_relativeSplatPath,
1234
1272
  opts?.fromRouteId,
1235
1273
  opts?.relative
1236
1274
  );
@@ -1363,6 +1401,7 @@ export function createRouter(init: RouterInit): Router {
1363
1401
  historyAction: HistoryAction,
1364
1402
  location: Location,
1365
1403
  opts?: {
1404
+ initialHydration?: boolean;
1366
1405
  submission?: Submission;
1367
1406
  fetcherSubmission?: Submission;
1368
1407
  overrideNavigation?: Navigation;
@@ -1487,6 +1526,7 @@ export function createRouter(init: RouterInit): Router {
1487
1526
  opts && opts.submission,
1488
1527
  opts && opts.fetcherSubmission,
1489
1528
  opts && opts.replace,
1529
+ opts && opts.initialHydration === true,
1490
1530
  flushSync,
1491
1531
  pendingActionData,
1492
1532
  pendingError
@@ -1545,7 +1585,8 @@ export function createRouter(init: RouterInit): Router {
1545
1585
  matches,
1546
1586
  manifest,
1547
1587
  mapRouteProperties,
1548
- basename
1588
+ basename,
1589
+ future.v7_relativeSplatPath
1549
1590
  );
1550
1591
 
1551
1592
  if (request.signal.aborted) {
@@ -1607,6 +1648,7 @@ export function createRouter(init: RouterInit): Router {
1607
1648
  submission?: Submission,
1608
1649
  fetcherSubmission?: Submission,
1609
1650
  replace?: boolean,
1651
+ initialHydration?: boolean,
1610
1652
  flushSync?: boolean,
1611
1653
  pendingActionData?: RouteData,
1612
1654
  pendingError?: RouteData
@@ -1629,6 +1671,7 @@ export function createRouter(init: RouterInit): Router {
1629
1671
  matches,
1630
1672
  activeSubmission,
1631
1673
  location,
1674
+ future.v7_partialHydration && initialHydration === true,
1632
1675
  isRevalidationRequired,
1633
1676
  cancelledDeferredRoutes,
1634
1677
  cancelledFetcherLoads,
@@ -1674,7 +1717,12 @@ export function createRouter(init: RouterInit): Router {
1674
1717
  // state. If not, we need to switch to our loading state and load data,
1675
1718
  // preserving any new action data or existing action data (in the case of
1676
1719
  // a revalidation interrupting an actionReload)
1677
- if (!isUninterruptedRevalidation) {
1720
+ // If we have partialHydration enabled, then don't update the state for the
1721
+ // initial data load since iot's not a "navigation"
1722
+ if (
1723
+ !isUninterruptedRevalidation &&
1724
+ (!future.v7_partialHydration || !initialHydration)
1725
+ ) {
1678
1726
  revalidatingFetchers.forEach((rf) => {
1679
1727
  let fetcher = state.fetchers.get(rf.key);
1680
1728
  let revalidatingFetcher = getLoadingFetcher(
@@ -1824,6 +1872,7 @@ export function createRouter(init: RouterInit): Router {
1824
1872
  basename,
1825
1873
  future.v7_prependBasename,
1826
1874
  href,
1875
+ future.v7_relativeSplatPath,
1827
1876
  routeId,
1828
1877
  opts?.relative
1829
1878
  );
@@ -1930,7 +1979,8 @@ export function createRouter(init: RouterInit): Router {
1930
1979
  requestMatches,
1931
1980
  manifest,
1932
1981
  mapRouteProperties,
1933
- basename
1982
+ basename,
1983
+ future.v7_relativeSplatPath
1934
1984
  );
1935
1985
 
1936
1986
  if (fetchRequest.signal.aborted) {
@@ -2003,6 +2053,7 @@ export function createRouter(init: RouterInit): Router {
2003
2053
  matches,
2004
2054
  submission,
2005
2055
  nextLocation,
2056
+ false,
2006
2057
  isRevalidationRequired,
2007
2058
  cancelledDeferredRoutes,
2008
2059
  cancelledFetcherLoads,
@@ -2173,7 +2224,8 @@ export function createRouter(init: RouterInit): Router {
2173
2224
  matches,
2174
2225
  manifest,
2175
2226
  mapRouteProperties,
2176
- basename
2227
+ basename,
2228
+ future.v7_relativeSplatPath
2177
2229
  );
2178
2230
 
2179
2231
  // Deferred isn't supported for fetcher loads, await everything and treat it
@@ -2369,7 +2421,8 @@ export function createRouter(init: RouterInit): Router {
2369
2421
  matches,
2370
2422
  manifest,
2371
2423
  mapRouteProperties,
2372
- basename
2424
+ basename,
2425
+ future.v7_relativeSplatPath
2373
2426
  )
2374
2427
  ),
2375
2428
  ...fetchersToLoad.map((f) => {
@@ -2381,7 +2434,8 @@ export function createRouter(init: RouterInit): Router {
2381
2434
  f.matches,
2382
2435
  manifest,
2383
2436
  mapRouteProperties,
2384
- basename
2437
+ basename,
2438
+ future.v7_relativeSplatPath
2385
2439
  );
2386
2440
  } else {
2387
2441
  let error: ErrorResult = {
@@ -2723,6 +2777,9 @@ export function createRouter(init: RouterInit): Router {
2723
2777
  get basename() {
2724
2778
  return basename;
2725
2779
  },
2780
+ get future() {
2781
+ return future;
2782
+ },
2726
2783
  get state() {
2727
2784
  return state;
2728
2785
  },
@@ -2764,6 +2821,13 @@ export function createRouter(init: RouterInit): Router {
2764
2821
 
2765
2822
  export const UNSAFE_DEFERRED_SYMBOL = Symbol("deferred");
2766
2823
 
2824
+ /**
2825
+ * Future flags to toggle new feature behavior
2826
+ */
2827
+ export interface StaticHandlerFutureConfig {
2828
+ v7_relativeSplatPath: boolean;
2829
+ }
2830
+
2767
2831
  export interface CreateStaticHandlerOptions {
2768
2832
  basename?: string;
2769
2833
  /**
@@ -2771,6 +2835,7 @@ export interface CreateStaticHandlerOptions {
2771
2835
  */
2772
2836
  detectErrorBoundary?: DetectErrorBoundaryFunction;
2773
2837
  mapRouteProperties?: MapRoutePropertiesFunction;
2838
+ future?: Partial<StaticHandlerFutureConfig>;
2774
2839
  }
2775
2840
 
2776
2841
  export function createStaticHandler(
@@ -2796,6 +2861,11 @@ export function createStaticHandler(
2796
2861
  } else {
2797
2862
  mapRouteProperties = defaultMapRouteProperties;
2798
2863
  }
2864
+ // Config driven behavior flags
2865
+ let future: StaticHandlerFutureConfig = {
2866
+ v7_relativeSplatPath: false,
2867
+ ...(opts ? opts.future : null),
2868
+ };
2799
2869
 
2800
2870
  let dataRoutes = convertRoutesToDataRoutes(
2801
2871
  routes,
@@ -3058,6 +3128,7 @@ export function createStaticHandler(
3058
3128
  manifest,
3059
3129
  mapRouteProperties,
3060
3130
  basename,
3131
+ future.v7_relativeSplatPath,
3061
3132
  { isStaticRequest: true, isRouteRequest, requestContext }
3062
3133
  );
3063
3134
 
@@ -3226,6 +3297,7 @@ export function createStaticHandler(
3226
3297
  manifest,
3227
3298
  mapRouteProperties,
3228
3299
  basename,
3300
+ future.v7_relativeSplatPath,
3229
3301
  { isStaticRequest: true, isRouteRequest, requestContext }
3230
3302
  )
3231
3303
  ),
@@ -3316,6 +3388,7 @@ function normalizeTo(
3316
3388
  basename: string,
3317
3389
  prependBasename: boolean,
3318
3390
  to: To | null,
3391
+ v7_relativeSplatPath: boolean,
3319
3392
  fromRouteId?: string,
3320
3393
  relative?: RelativeRoutingType
3321
3394
  ) {
@@ -3340,7 +3413,7 @@ function normalizeTo(
3340
3413
  // Resolve the relative path
3341
3414
  let path = resolveTo(
3342
3415
  to ? to : ".",
3343
- getPathContributingMatches(contextualMatches).map((m) => m.pathnameBase),
3416
+ getResolveToMatches(contextualMatches, v7_relativeSplatPath),
3344
3417
  stripBasename(location.pathname, basename) || location.pathname,
3345
3418
  relative === "path"
3346
3419
  );
@@ -3548,6 +3621,7 @@ function getMatchesToLoad(
3548
3621
  matches: AgnosticDataRouteMatch[],
3549
3622
  submission: Submission | undefined,
3550
3623
  location: Location,
3624
+ isInitialLoad: boolean,
3551
3625
  isRevalidationRequired: boolean,
3552
3626
  cancelledDeferredRoutes: string[],
3553
3627
  cancelledFetcherLoads: string[],
@@ -3573,10 +3647,17 @@ function getMatchesToLoad(
3573
3647
  let boundaryMatches = getLoaderMatchesUntilBoundary(matches, boundaryId);
3574
3648
 
3575
3649
  let navigationMatches = boundaryMatches.filter((match, index) => {
3650
+ if (isInitialLoad) {
3651
+ // On initial hydration we don't do any shouldRevalidate stuff - we just
3652
+ // call the unhydrated loaders
3653
+ return isUnhydratedRoute(state, match.route);
3654
+ }
3655
+
3576
3656
  if (match.route.lazy) {
3577
3657
  // We haven't loaded this route yet so we don't know if it's got a loader!
3578
3658
  return true;
3579
3659
  }
3660
+
3580
3661
  if (match.route.loader == null) {
3581
3662
  return false;
3582
3663
  }
@@ -3618,8 +3699,13 @@ function getMatchesToLoad(
3618
3699
  // Pick fetcher.loads that need to be revalidated
3619
3700
  let revalidatingFetchers: RevalidatingFetcher[] = [];
3620
3701
  fetchLoadMatches.forEach((f, key) => {
3621
- // Don't revalidate if fetcher won't be present in the subsequent render
3702
+ // Don't revalidate:
3703
+ // - on initial load (shouldn't be any fetchers then anyway)
3704
+ // - if fetcher won't be present in the subsequent render
3705
+ // - no longer matches the URL (v7_fetcherPersist=false)
3706
+ // - was unmounted but persisted due to v7_fetcherPersist=true
3622
3707
  if (
3708
+ isInitialLoad ||
3623
3709
  !matches.some((m) => m.route.id === f.routeId) ||
3624
3710
  deletedFetchers.has(key)
3625
3711
  ) {
@@ -3695,6 +3781,23 @@ function getMatchesToLoad(
3695
3781
  return [navigationMatches, revalidatingFetchers];
3696
3782
  }
3697
3783
 
3784
+ // Is this route unhydrated (when v7_partialHydration=true) such that we need
3785
+ // to call it's loader on the initial router creation
3786
+ function isUnhydratedRoute(state: RouterState, route: AgnosticDataRouteObject) {
3787
+ if (!route.loader) {
3788
+ return false;
3789
+ }
3790
+ if (route.loader.hydrate) {
3791
+ return true;
3792
+ }
3793
+ return (
3794
+ state.loaderData[route.id] === undefined &&
3795
+ (!state.errors ||
3796
+ // Loader ran but errored - don't re-run
3797
+ state.errors[route.id] === undefined)
3798
+ );
3799
+ }
3800
+
3698
3801
  function isNewLoader(
3699
3802
  currentLoaderData: RouteData,
3700
3803
  currentMatch: AgnosticDataRouteMatch,
@@ -3830,6 +3933,7 @@ async function callLoaderOrAction(
3830
3933
  manifest: RouteManifest,
3831
3934
  mapRouteProperties: MapRoutePropertiesFunction,
3832
3935
  basename: string,
3936
+ v7_relativeSplatPath: boolean,
3833
3937
  opts: {
3834
3938
  isStaticRequest?: boolean;
3835
3939
  isRouteRequest?: boolean;
@@ -3943,7 +4047,8 @@ async function callLoaderOrAction(
3943
4047
  matches.slice(0, matches.indexOf(match) + 1),
3944
4048
  basename,
3945
4049
  true,
3946
- location
4050
+ location,
4051
+ v7_relativeSplatPath
3947
4052
  );
3948
4053
  } else if (!opts.isStaticRequest) {
3949
4054
  // Strip off the protocol+origin for same-origin + same-basename absolute
@@ -3990,13 +4095,18 @@ async function callLoaderOrAction(
3990
4095
  }
3991
4096
 
3992
4097
  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();
4098
+
4099
+ try {
4100
+ let contentType = result.headers.get("Content-Type");
4101
+ // Check between word boundaries instead of startsWith() due to the last
4102
+ // paragraph of https://httpwg.org/specs/rfc9110.html#field.content-type
4103
+ if (contentType && /\bapplication\/json\b/.test(contentType)) {
4104
+ data = await result.json();
4105
+ } else {
4106
+ data = await result.text();
4107
+ }
4108
+ } catch (e) {
4109
+ return { type: ResultType.error, error: e };
4000
4110
  }
4001
4111
 
4002
4112
  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("/");