@remix-run/router 1.18.0 → 1.19.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
@@ -55,7 +55,6 @@ export type DataResult = SuccessResult | DeferredResult | RedirectResult | Error
55
55
  export interface HandlerResult {
56
56
  type: "data" | "error";
57
57
  result: unknown;
58
- status?: number;
59
58
  }
60
59
  type LowerCaseFormMethod = "get" | "post" | "put" | "patch" | "delete";
61
60
  type UpperCaseFormMethod = Uppercase<LowerCaseFormMethod>;
@@ -459,6 +458,17 @@ export type JsonFunction = <Data>(data: Data, init?: number | ResponseInit) => R
459
458
  * to JSON and sets the `Content-Type` header.
460
459
  */
461
460
  export declare const json: JsonFunction;
461
+ export declare class DataWithResponseInit<D> {
462
+ type: string;
463
+ data: D;
464
+ init: ResponseInit | null;
465
+ constructor(data: D, init?: ResponseInit);
466
+ }
467
+ /**
468
+ * Create "responses" that contain `status`/`headers` without forcing
469
+ * serialization into an actual `Response` - used by Remix single fetch
470
+ */
471
+ export declare function data<D>(data: D, init?: number | ResponseInit): DataWithResponseInit<D>;
462
472
  export interface TrackedPromise extends Promise<any> {
463
473
  _tracked?: boolean;
464
474
  _data?: any;
@@ -500,6 +510,13 @@ export declare const redirect: RedirectFunction;
500
510
  * Defaults to "302 Found".
501
511
  */
502
512
  export declare const redirectDocument: RedirectFunction;
513
+ /**
514
+ * A redirect response that will perform a `history.replaceState` instead of a
515
+ * `history.pushState` for client-side navigation redirects.
516
+ * Sets the status code and the `Location` header.
517
+ * Defaults to "302 Found".
518
+ */
519
+ export declare const replace: RedirectFunction;
503
520
  export type ErrorResponse = {
504
521
  status: number;
505
522
  statusText: string;
package/index.ts CHANGED
@@ -33,10 +33,12 @@ export type {
33
33
  TrackedPromise,
34
34
  UIMatch,
35
35
  V7_FormMethod,
36
+ DataWithResponseInit as UNSAFE_DataWithResponseInit,
36
37
  } from "./utils";
37
38
 
38
39
  export {
39
40
  AbortedDeferredError,
41
+ data as unstable_data,
40
42
  defer,
41
43
  generatePath,
42
44
  getToPathname,
@@ -48,6 +50,7 @@ export {
48
50
  normalizePathname,
49
51
  redirect,
50
52
  redirectDocument,
53
+ replace,
51
54
  resolvePath,
52
55
  resolveTo,
53
56
  stripBasename,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remix-run/router",
3
- "version": "1.18.0",
3
+ "version": "1.19.0-pre.1",
4
4
  "description": "Nested/Data-driven/Framework-agnostic Routing",
5
5
  "keywords": [
6
6
  "remix",
package/router.ts CHANGED
@@ -36,6 +36,7 @@ import type {
36
36
  V7_FormMethod,
37
37
  V7_MutationFormMethod,
38
38
  AgnosticPatchRoutesOnMissFunction,
39
+ DataWithResponseInit,
39
40
  } from "./utils";
40
41
  import {
41
42
  ErrorResponseImpl,
@@ -847,7 +848,7 @@ export function createRouter(init: RouterInit): Router {
847
848
  // In SSR apps (with `hydrationData`), we expect that the server will send
848
849
  // up the proper matched routes so we don't want to run lazy discovery on
849
850
  // initial hydration and want to hydrate into the splat route.
850
- if (initialMatches && patchRoutesOnMissImpl && !init.hydrationData) {
851
+ if (initialMatches && !init.hydrationData) {
851
852
  let fogOfWar = checkFogOfWar(
852
853
  initialMatches,
853
854
  dataRoutes,
@@ -860,9 +861,22 @@ export function createRouter(init: RouterInit): Router {
860
861
 
861
862
  let initialized: boolean;
862
863
  if (!initialMatches) {
863
- // We need to run patchRoutesOnMiss in initialize()
864
864
  initialized = false;
865
865
  initialMatches = [];
866
+
867
+ // If partial hydration and fog of war is enabled, we will be running
868
+ // `patchRoutesOnMiss` during hydration so include any partial matches as
869
+ // the initial matches so we can properly render `HydrateFallback`'s
870
+ if (future.v7_partialHydration) {
871
+ let fogOfWar = checkFogOfWar(
872
+ null,
873
+ dataRoutes,
874
+ init.history.location.pathname
875
+ );
876
+ if (fogOfWar.active && fogOfWar.matches) {
877
+ initialMatches = fogOfWar.matches;
878
+ }
879
+ }
866
880
  } else if (initialMatches.some((m) => m.route.lazy)) {
867
881
  // All initialMatches need to be loaded before we're ready. If we have lazy
868
882
  // functions around still then we'll need to run them in initialize()
@@ -967,7 +981,7 @@ export function createRouter(init: RouterInit): Router {
967
981
 
968
982
  // Use this internal array to capture fetcher loads that were cancelled by an
969
983
  // action navigation and require revalidation
970
- let cancelledFetcherLoads: string[] = [];
984
+ let cancelledFetcherLoads: Set<string> = new Set();
971
985
 
972
986
  // AbortControllers for any in-flight fetchers
973
987
  let fetchControllers = new Map<string, AbortController>();
@@ -1320,7 +1334,6 @@ export function createRouter(init: RouterInit): Router {
1320
1334
  isUninterruptedRevalidation = false;
1321
1335
  isRevalidationRequired = false;
1322
1336
  cancelledDeferredRoutes = [];
1323
- cancelledFetcherLoads = [];
1324
1337
  }
1325
1338
 
1326
1339
  // Trigger a navigation event, which can either be a numerical POP or a PUSH
@@ -2683,7 +2696,9 @@ export function createRouter(init: RouterInit): Router {
2683
2696
  pendingNavigationController = null;
2684
2697
 
2685
2698
  let redirectHistoryAction =
2686
- replace === true ? HistoryAction.Replace : HistoryAction.Push;
2699
+ replace === true || redirect.response.headers.has("X-Remix-Replace")
2700
+ ? HistoryAction.Replace
2701
+ : HistoryAction.Push;
2687
2702
 
2688
2703
  // Use the incoming submission if provided, fallback on the active one in
2689
2704
  // state.navigation
@@ -2851,7 +2866,7 @@ export function createRouter(init: RouterInit): Router {
2851
2866
  // Abort in-flight fetcher loads
2852
2867
  fetchLoadMatches.forEach((_, key) => {
2853
2868
  if (fetchControllers.has(key)) {
2854
- cancelledFetcherLoads.push(key);
2869
+ cancelledFetcherLoads.add(key);
2855
2870
  abortFetcher(key);
2856
2871
  }
2857
2872
  });
@@ -2915,6 +2930,7 @@ export function createRouter(init: RouterInit): Router {
2915
2930
  fetchReloadIds.delete(key);
2916
2931
  fetchRedirectIds.delete(key);
2917
2932
  deletedFetchers.delete(key);
2933
+ cancelledFetcherLoads.delete(key);
2918
2934
  state.fetchers.delete(key);
2919
2935
  }
2920
2936
 
@@ -4308,7 +4324,7 @@ function getMatchesToLoad(
4308
4324
  skipActionErrorRevalidation: boolean,
4309
4325
  isRevalidationRequired: boolean,
4310
4326
  cancelledDeferredRoutes: string[],
4311
- cancelledFetcherLoads: string[],
4327
+ cancelledFetcherLoads: Set<string>,
4312
4328
  deletedFetchers: Set<string>,
4313
4329
  fetchLoadMatches: Map<string, FetchLoadMatch>,
4314
4330
  fetchRedirectIds: Set<string>,
@@ -4443,8 +4459,9 @@ function getMatchesToLoad(
4443
4459
  if (fetchRedirectIds.has(key)) {
4444
4460
  // Never trigger a revalidation of an actively redirecting fetcher
4445
4461
  shouldRevalidate = false;
4446
- } else if (cancelledFetcherLoads.includes(key)) {
4447
- // Always revalidate if the fetcher was cancelled
4462
+ } else if (cancelledFetcherLoads.has(key)) {
4463
+ // Always mark for revalidation if the fetcher was cancelled
4464
+ cancelledFetcherLoads.delete(key);
4448
4465
  shouldRevalidate = true;
4449
4466
  } else if (
4450
4467
  fetcher &&
@@ -4904,7 +4921,7 @@ async function callLoaderOrAction(
4904
4921
  async function convertHandlerResultToDataResult(
4905
4922
  handlerResult: HandlerResult
4906
4923
  ): Promise<DataResult> {
4907
- let { result, type, status } = handlerResult;
4924
+ let { result, type } = handlerResult;
4908
4925
 
4909
4926
  if (isResponse(result)) {
4910
4927
  let data: any;
@@ -4944,10 +4961,26 @@ async function convertHandlerResultToDataResult(
4944
4961
  }
4945
4962
 
4946
4963
  if (type === ResultType.error) {
4964
+ if (isDataWithResponseInit(result)) {
4965
+ if (result.data instanceof Error) {
4966
+ return {
4967
+ type: ResultType.error,
4968
+ error: result.data,
4969
+ statusCode: result.init?.status,
4970
+ };
4971
+ }
4972
+
4973
+ // Convert thrown unstable_data() to ErrorResponse instances
4974
+ result = new ErrorResponseImpl(
4975
+ result.init?.status || 500,
4976
+ undefined,
4977
+ result.data
4978
+ );
4979
+ }
4947
4980
  return {
4948
4981
  type: ResultType.error,
4949
4982
  error: result,
4950
- statusCode: isRouteErrorResponse(result) ? result.status : status,
4983
+ statusCode: isRouteErrorResponse(result) ? result.status : undefined,
4951
4984
  };
4952
4985
  }
4953
4986
 
@@ -4960,7 +4993,18 @@ async function convertHandlerResultToDataResult(
4960
4993
  };
4961
4994
  }
4962
4995
 
4963
- return { type: ResultType.data, data: result, statusCode: status };
4996
+ if (isDataWithResponseInit(result)) {
4997
+ return {
4998
+ type: ResultType.data,
4999
+ data: result.data,
5000
+ statusCode: result.init?.status,
5001
+ headers: result.init?.headers
5002
+ ? new Headers(result.init.headers)
5003
+ : undefined,
5004
+ };
5005
+ }
5006
+
5007
+ return { type: ResultType.data, data: result };
4964
5008
  }
4965
5009
 
4966
5010
  // Support relative routing in internal redirects
@@ -5474,6 +5518,19 @@ function isRedirectResult(result?: DataResult): result is RedirectResult {
5474
5518
  return (result && result.type) === ResultType.redirect;
5475
5519
  }
5476
5520
 
5521
+ export function isDataWithResponseInit(
5522
+ value: any
5523
+ ): value is DataWithResponseInit<unknown> {
5524
+ return (
5525
+ typeof value === "object" &&
5526
+ value != null &&
5527
+ "type" in value &&
5528
+ "data" in value &&
5529
+ "init" in value &&
5530
+ value.type === "DataWithResponseInit"
5531
+ );
5532
+ }
5533
+
5477
5534
  export function isDeferredData(value: any): value is DeferredData {
5478
5535
  let deferred: DeferredData = value;
5479
5536
  return (
package/utils.ts CHANGED
@@ -68,8 +68,7 @@ export type DataResult =
68
68
  */
69
69
  export interface HandlerResult {
70
70
  type: "data" | "error";
71
- result: unknown; // data, Error, Response, DeferredData
72
- status?: number;
71
+ result: unknown; // data, Error, Response, DeferredData, DataWithResponseInit
73
72
  }
74
73
 
75
74
  type LowerCaseFormMethod = "get" | "post" | "put" | "patch" | "delete";
@@ -1375,6 +1374,28 @@ export const json: JsonFunction = (data, init = {}) => {
1375
1374
  });
1376
1375
  };
1377
1376
 
1377
+ export class DataWithResponseInit<D> {
1378
+ type: string = "DataWithResponseInit";
1379
+ data: D;
1380
+ init: ResponseInit | null;
1381
+
1382
+ constructor(data: D, init?: ResponseInit) {
1383
+ this.data = data;
1384
+ this.init = init || null;
1385
+ }
1386
+ }
1387
+
1388
+ /**
1389
+ * Create "responses" that contain `status`/`headers` without forcing
1390
+ * serialization into an actual `Response` - used by Remix single fetch
1391
+ */
1392
+ export function data<D>(data: D, init?: number | ResponseInit) {
1393
+ return new DataWithResponseInit(
1394
+ data,
1395
+ typeof init === "number" ? { status: init } : init
1396
+ );
1397
+ }
1398
+
1378
1399
  export interface TrackedPromise extends Promise<any> {
1379
1400
  _tracked?: boolean;
1380
1401
  _data?: any;
@@ -1619,6 +1640,18 @@ export const redirectDocument: RedirectFunction = (url, init) => {
1619
1640
  return response;
1620
1641
  };
1621
1642
 
1643
+ /**
1644
+ * A redirect response that will perform a `history.replaceState` instead of a
1645
+ * `history.pushState` for client-side navigation redirects.
1646
+ * Sets the status code and the `Location` header.
1647
+ * Defaults to "302 Found".
1648
+ */
1649
+ export const replace: RedirectFunction = (url, init) => {
1650
+ let response = redirect(url, init);
1651
+ response.headers.set("X-Remix-Replace", "true");
1652
+ return response;
1653
+ };
1654
+
1622
1655
  export type ErrorResponse = {
1623
1656
  status: number;
1624
1657
  statusText: string;