@remix-run/router 1.4.0 → 1.5.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
@@ -50,8 +50,25 @@ export interface ErrorResult {
50
50
  * Result from a loader or action - potentially successful or unsuccessful
51
51
  */
52
52
  export declare type DataResult = SuccessResult | DeferredResult | RedirectResult | ErrorResult;
53
- export declare type MutationFormMethod = "post" | "put" | "patch" | "delete";
54
- export declare type FormMethod = "get" | MutationFormMethod;
53
+ declare type LowerCaseFormMethod = "get" | "post" | "put" | "patch" | "delete";
54
+ declare type UpperCaseFormMethod = Uppercase<LowerCaseFormMethod>;
55
+ /**
56
+ * Users can specify either lowercase or uppercase form methods on <Form>,
57
+ * useSubmit(), <fetcher.Form>, etc.
58
+ */
59
+ export declare type HTMLFormMethod = LowerCaseFormMethod | UpperCaseFormMethod;
60
+ /**
61
+ * Active navigation/fetcher form methods are exposed in lowercase on the
62
+ * RouterState
63
+ */
64
+ export declare type FormMethod = LowerCaseFormMethod;
65
+ export declare type MutationFormMethod = Exclude<FormMethod, "get">;
66
+ /**
67
+ * In v7, active navigation/fetcher form methods are exposed in uppercase on the
68
+ * RouterState. This is to align with the normalization done via fetch().
69
+ */
70
+ export declare type V7_FormMethod = UpperCaseFormMethod;
71
+ export declare type V7_MutationFormMethod = Exclude<V7_FormMethod, "GET">;
55
72
  export declare type FormEncType = "application/x-www-form-urlencoded" | "multipart/form-data";
56
73
  /**
57
74
  * @private
@@ -59,7 +76,7 @@ export declare type FormEncType = "application/x-www-form-urlencoded" | "multipa
59
76
  * external consumption
60
77
  */
61
78
  export interface Submission {
62
- formMethod: FormMethod;
79
+ formMethod: FormMethod | V7_FormMethod;
63
80
  formAction: string;
64
81
  formEncType: FormEncType;
65
82
  formData: FormData;
package/index.ts CHANGED
@@ -13,6 +13,7 @@ export type {
13
13
  TrackedPromise,
14
14
  FormEncType,
15
15
  FormMethod,
16
+ HTMLFormMethod,
16
17
  JsonFunction,
17
18
  LoaderFunction,
18
19
  LoaderFunctionArgs,
@@ -22,7 +23,7 @@ export type {
22
23
  PathPattern,
23
24
  RedirectFunction,
24
25
  ShouldRevalidateFunction,
25
- Submission,
26
+ V7_FormMethod,
26
27
  } from "./utils";
27
28
 
28
29
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remix-run/router",
3
- "version": "1.4.0",
3
+ "version": "1.5.0-pre.0",
4
4
  "description": "Nested/Data-driven/Framework-agnostic Routing",
5
5
  "keywords": [
6
6
  "remix",
package/router.ts CHANGED
@@ -22,12 +22,15 @@ import type {
22
22
  Submission,
23
23
  SuccessResult,
24
24
  AgnosticRouteMatch,
25
- MutationFormMethod,
26
25
  ShouldRevalidateFunction,
27
26
  RouteManifest,
28
27
  ImmutableRouteKey,
29
28
  ActionFunction,
30
29
  LoaderFunction,
30
+ V7_MutationFormMethod,
31
+ V7_FormMethod,
32
+ HTMLFormMethod,
33
+ MutationFormMethod,
31
34
  } from "./utils";
32
35
  import {
33
36
  DeferredData,
@@ -326,15 +329,23 @@ export type HydrationState = Partial<
326
329
  Pick<RouterState, "loaderData" | "actionData" | "errors">
327
330
  >;
328
331
 
332
+ /**
333
+ * Future flags to toggle new feature behavior
334
+ */
335
+ export interface FutureConfig {
336
+ v7_normalizeFormMethod: boolean;
337
+ }
338
+
329
339
  /**
330
340
  * Initialization options for createRouter
331
341
  */
332
342
  export interface RouterInit {
333
- basename?: string;
334
343
  routes: AgnosticRouteObject[];
335
344
  history: History;
336
- hydrationData?: HydrationState;
345
+ basename?: string;
337
346
  detectErrorBoundary?: DetectErrorBoundaryFunction;
347
+ future?: FutureConfig;
348
+ hydrationData?: HydrationState;
338
349
  }
339
350
 
340
351
  /**
@@ -415,7 +426,7 @@ type SubmissionNavigateOptions = {
415
426
  replace?: boolean;
416
427
  state?: any;
417
428
  preventScrollReset?: boolean;
418
- formMethod?: FormMethod;
429
+ formMethod?: HTMLFormMethod;
419
430
  formEncType?: FormEncType;
420
431
  formData: FormData;
421
432
  };
@@ -449,7 +460,7 @@ export type NavigationStates = {
449
460
  Loading: {
450
461
  state: "loading";
451
462
  location: Location;
452
- formMethod: FormMethod | undefined;
463
+ formMethod: FormMethod | V7_FormMethod | undefined;
453
464
  formAction: string | undefined;
454
465
  formEncType: FormEncType | undefined;
455
466
  formData: FormData | undefined;
@@ -457,7 +468,7 @@ export type NavigationStates = {
457
468
  Submitting: {
458
469
  state: "submitting";
459
470
  location: Location;
460
- formMethod: FormMethod;
471
+ formMethod: FormMethod | V7_FormMethod;
461
472
  formAction: string;
462
473
  formEncType: FormEncType;
463
474
  formData: FormData;
@@ -483,7 +494,7 @@ type FetcherStates<TData = any> = {
483
494
  };
484
495
  Loading: {
485
496
  state: "loading";
486
- formMethod: FormMethod | undefined;
497
+ formMethod: FormMethod | V7_FormMethod | undefined;
487
498
  formAction: string | undefined;
488
499
  formEncType: FormEncType | undefined;
489
500
  formData: FormData | undefined;
@@ -492,7 +503,7 @@ type FetcherStates<TData = any> = {
492
503
  };
493
504
  Submitting: {
494
505
  state: "submitting";
495
- formMethod: FormMethod;
506
+ formMethod: FormMethod | V7_FormMethod;
496
507
  formAction: string;
497
508
  formEncType: FormEncType;
498
509
  formData: FormData;
@@ -676,6 +687,11 @@ export function createRouter(init: RouterInit): Router {
676
687
  manifest
677
688
  );
678
689
  let inFlightDataRoutes: AgnosticDataRouteObject[] | undefined;
690
+ // Config driven behavior flags
691
+ let future: FutureConfig = {
692
+ v7_normalizeFormMethod: false,
693
+ ...init.future,
694
+ };
679
695
  // Cleanup function for history
680
696
  let unlistenHistory: (() => void) | null = null;
681
697
  // Externally-provided functions to call on all state changes
@@ -862,35 +878,15 @@ export function createRouter(init: RouterInit): Router {
862
878
  }
863
879
  );
864
880
 
865
- if (state.initialized) {
866
- return router;
867
- }
868
-
869
- let lazyMatches = state.matches.filter((m) => m.route.lazy);
870
-
871
- if (lazyMatches.length === 0) {
872
- // Kick off initial data load if needed. Use Pop to avoid modifying history
881
+ // Kick off initial data load if needed. Use Pop to avoid modifying history
882
+ // Note we don't do any handling of lazy here. For SPA's it'll get handled
883
+ // in the normal navigation flow. For SSR it's expected that lazy modules are
884
+ // resolved prior to router creation since we can't go into a fallbackElement
885
+ // UI for SSR'd apps
886
+ if (!state.initialized) {
873
887
  startNavigation(HistoryAction.Pop, state.location);
874
- return router;
875
888
  }
876
889
 
877
- // Load lazy modules, then kick off initial data load if needed
878
- let lazyPromises = lazyMatches.map((m) =>
879
- loadLazyRouteModule(m.route, detectErrorBoundary, manifest)
880
- );
881
- Promise.all(lazyPromises).then(() => {
882
- let initialized =
883
- !state.matches.some((m) => m.route.loader) ||
884
- init.hydrationData != null;
885
- if (initialized) {
886
- // We already have required loaderData so we can just set initialized
887
- updateState({ initialized: true });
888
- } else {
889
- // We still need to kick off initial data loads
890
- startNavigation(HistoryAction.Pop, state.location);
891
- }
892
- });
893
-
894
890
  return router;
895
891
  }
896
892
 
@@ -1033,7 +1029,11 @@ export function createRouter(init: RouterInit): Router {
1033
1029
  return;
1034
1030
  }
1035
1031
 
1036
- let { path, submission, error } = normalizeNavigateOptions(to, opts);
1032
+ let { path, submission, error } = normalizeNavigateOptions(
1033
+ to,
1034
+ future,
1035
+ opts
1036
+ );
1037
1037
 
1038
1038
  let currentLocation = state.location;
1039
1039
  let nextLocation = createLocation(state.location, path, opts && opts.state);
@@ -1152,6 +1152,7 @@ export function createRouter(init: RouterInit): Router {
1152
1152
  location: Location,
1153
1153
  opts?: {
1154
1154
  submission?: Submission;
1155
+ fetcherSubmission?: Submission;
1155
1156
  overrideNavigation?: Navigation;
1156
1157
  pendingError?: ErrorResponse;
1157
1158
  startUninterruptedRevalidation?: boolean;
@@ -1263,6 +1264,7 @@ export function createRouter(init: RouterInit): Router {
1263
1264
  matches,
1264
1265
  loadingNavigation,
1265
1266
  opts && opts.submission,
1267
+ opts && opts.fetcherSubmission,
1266
1268
  opts && opts.replace,
1267
1269
  pendingActionData,
1268
1270
  pendingError
@@ -1385,6 +1387,7 @@ export function createRouter(init: RouterInit): Router {
1385
1387
  matches: AgnosticDataRouteMatch[],
1386
1388
  overrideNavigation?: Navigation,
1387
1389
  submission?: Submission,
1390
+ fetcherSubmission?: Submission,
1388
1391
  replace?: boolean,
1389
1392
  pendingActionData?: RouteData,
1390
1393
  pendingError?: RouteData
@@ -1406,19 +1409,20 @@ export function createRouter(init: RouterInit): Router {
1406
1409
 
1407
1410
  // If this was a redirect from an action we don't have a "submission" but
1408
1411
  // we have it on the loading navigation so use that if available
1409
- let activeSubmission = submission
1410
- ? submission
1411
- : loadingNavigation.formMethod &&
1412
- loadingNavigation.formAction &&
1413
- loadingNavigation.formData &&
1414
- loadingNavigation.formEncType
1415
- ? {
1416
- formMethod: loadingNavigation.formMethod,
1417
- formAction: loadingNavigation.formAction,
1418
- formData: loadingNavigation.formData,
1419
- formEncType: loadingNavigation.formEncType,
1420
- }
1421
- : undefined;
1412
+ let activeSubmission =
1413
+ submission || fetcherSubmission
1414
+ ? submission || fetcherSubmission
1415
+ : loadingNavigation.formMethod &&
1416
+ loadingNavigation.formAction &&
1417
+ loadingNavigation.formData &&
1418
+ loadingNavigation.formEncType
1419
+ ? {
1420
+ formMethod: loadingNavigation.formMethod,
1421
+ formAction: loadingNavigation.formAction,
1422
+ formData: loadingNavigation.formData,
1423
+ formEncType: loadingNavigation.formEncType,
1424
+ }
1425
+ : undefined;
1422
1426
 
1423
1427
  let routesToUse = inFlightDataRoutes || dataRoutes;
1424
1428
  let [matchesToLoad, revalidatingFetchers] = getMatchesToLoad(
@@ -1588,7 +1592,12 @@ export function createRouter(init: RouterInit): Router {
1588
1592
  return;
1589
1593
  }
1590
1594
 
1591
- let { path, submission } = normalizeNavigateOptions(href, opts, true);
1595
+ let { path, submission } = normalizeNavigateOptions(
1596
+ href,
1597
+ future,
1598
+ opts,
1599
+ true
1600
+ );
1592
1601
  let match = getTargetMatch(matches, path);
1593
1602
 
1594
1603
  pendingPreventScrollReset = (opts && opts.preventScrollReset) === true;
@@ -1680,6 +1689,7 @@ export function createRouter(init: RouterInit): Router {
1680
1689
  updateState({ fetchers: new Map(state.fetchers) });
1681
1690
 
1682
1691
  return startRedirectNavigation(state, actionResult, {
1692
+ submission,
1683
1693
  isFetchActionRedirect: true,
1684
1694
  });
1685
1695
  }
@@ -2047,6 +2057,22 @@ export function createRouter(init: RouterInit): Router {
2047
2057
  // Preserve this flag across redirects
2048
2058
  preventScrollReset: pendingPreventScrollReset,
2049
2059
  });
2060
+ } else if (isFetchActionRedirect) {
2061
+ // For a fetch action redirect, we kick off a new loading navigation
2062
+ // without the fetcher submission, but we send it along for shouldRevalidate
2063
+ await startNavigation(redirectHistoryAction, redirectLocation, {
2064
+ overrideNavigation: {
2065
+ state: "loading",
2066
+ location: redirectLocation,
2067
+ formMethod: undefined,
2068
+ formAction: undefined,
2069
+ formEncType: undefined,
2070
+ formData: undefined,
2071
+ },
2072
+ fetcherSubmission: submission,
2073
+ // Preserve this flag across redirects
2074
+ preventScrollReset: pendingPreventScrollReset,
2075
+ });
2050
2076
  } else {
2051
2077
  // Otherwise, we kick off a new loading navigation, preserving the
2052
2078
  // submission info for the duration of this navigation
@@ -2461,12 +2487,12 @@ export function createStaticHandler(
2461
2487
  { requestContext }: { requestContext?: unknown } = {}
2462
2488
  ): Promise<StaticHandlerContext | Response> {
2463
2489
  let url = new URL(request.url);
2464
- let method = request.method.toLowerCase();
2490
+ let method = request.method;
2465
2491
  let location = createLocation("", createPath(url), null, "default");
2466
2492
  let matches = matchRoutes(dataRoutes, location, basename);
2467
2493
 
2468
2494
  // SSR supports HEAD requests while SPA doesn't
2469
- if (!isValidMethod(method) && method !== "head") {
2495
+ if (!isValidMethod(method) && method !== "HEAD") {
2470
2496
  let error = getInternalRouterError(405, { method });
2471
2497
  let { matches: methodNotAllowedMatches, route } =
2472
2498
  getShortCircuitMatches(dataRoutes);
@@ -2543,12 +2569,12 @@ export function createStaticHandler(
2543
2569
  }: { requestContext?: unknown; routeId?: string } = {}
2544
2570
  ): Promise<any> {
2545
2571
  let url = new URL(request.url);
2546
- let method = request.method.toLowerCase();
2572
+ let method = request.method;
2547
2573
  let location = createLocation("", createPath(url), null, "default");
2548
2574
  let matches = matchRoutes(dataRoutes, location, basename);
2549
2575
 
2550
2576
  // SSR supports HEAD requests while SPA doesn't
2551
- if (!isValidMethod(method) && method !== "head" && method !== "options") {
2577
+ if (!isValidMethod(method) && method !== "HEAD" && method !== "OPTIONS") {
2552
2578
  throw getInternalRouterError(405, { method });
2553
2579
  } else if (!matches) {
2554
2580
  throw getInternalRouterError(404, { pathname: location.pathname });
@@ -2943,6 +2969,7 @@ function isSubmissionNavigation(
2943
2969
  // URLSearchParams so they behave identically to links with query params
2944
2970
  function normalizeNavigateOptions(
2945
2971
  to: To,
2972
+ future: FutureConfig,
2946
2973
  opts?: RouterNavigateOptions,
2947
2974
  isFetcher = false
2948
2975
  ): {
@@ -2967,8 +2994,11 @@ function normalizeNavigateOptions(
2967
2994
  // Create a Submission on non-GET navigations
2968
2995
  let submission: Submission | undefined;
2969
2996
  if (opts.formData) {
2997
+ let formMethod = opts.formMethod || "get";
2970
2998
  submission = {
2971
- formMethod: opts.formMethod || "get",
2999
+ formMethod: future.v7_normalizeFormMethod
3000
+ ? (formMethod.toUpperCase() as V7_FormMethod)
3001
+ : (formMethod.toLowerCase() as FormMethod),
2972
3002
  formAction: stripHashFromPath(path),
2973
3003
  formEncType:
2974
3004
  (opts && opts.formEncType) || "application/x-www-form-urlencoded",
@@ -3482,6 +3512,9 @@ function createClientSideRequest(
3482
3512
 
3483
3513
  if (submission && isMutationMethod(submission.formMethod)) {
3484
3514
  let { formMethod, formEncType, formData } = submission;
3515
+ // Didn't think we needed this but it turns out unlike other methods, patch
3516
+ // won't be properly normalized to uppercase and results in a 405 error.
3517
+ // See: https://fetch.spec.whatwg.org/#concept-method
3485
3518
  init.method = formMethod.toUpperCase();
3486
3519
  init.body =
3487
3520
  formEncType === "application/x-www-form-urlencoded"
@@ -3851,12 +3884,14 @@ function isQueryRouteResponse(obj: any): obj is QueryRouteResponse {
3851
3884
  );
3852
3885
  }
3853
3886
 
3854
- function isValidMethod(method: string): method is FormMethod {
3855
- return validRequestMethods.has(method as FormMethod);
3887
+ function isValidMethod(method: string): method is FormMethod | V7_FormMethod {
3888
+ return validRequestMethods.has(method.toLowerCase() as FormMethod);
3856
3889
  }
3857
3890
 
3858
- function isMutationMethod(method?: string): method is MutationFormMethod {
3859
- return validMutationMethods.has(method as MutationFormMethod);
3891
+ function isMutationMethod(
3892
+ method: string
3893
+ ): method is MutationFormMethod | V7_MutationFormMethod {
3894
+ return validMutationMethods.has(method.toLowerCase() as MutationFormMethod);
3860
3895
  }
3861
3896
 
3862
3897
  async function resolveDeferredResults(
package/utils.ts CHANGED
@@ -63,8 +63,28 @@ export type DataResult =
63
63
  | RedirectResult
64
64
  | ErrorResult;
65
65
 
66
- export type MutationFormMethod = "post" | "put" | "patch" | "delete";
67
- export type FormMethod = "get" | MutationFormMethod;
66
+ type LowerCaseFormMethod = "get" | "post" | "put" | "patch" | "delete";
67
+ type UpperCaseFormMethod = Uppercase<LowerCaseFormMethod>;
68
+
69
+ /**
70
+ * Users can specify either lowercase or uppercase form methods on <Form>,
71
+ * useSubmit(), <fetcher.Form>, etc.
72
+ */
73
+ export type HTMLFormMethod = LowerCaseFormMethod | UpperCaseFormMethod;
74
+
75
+ /**
76
+ * Active navigation/fetcher form methods are exposed in lowercase on the
77
+ * RouterState
78
+ */
79
+ export type FormMethod = LowerCaseFormMethod;
80
+ export type MutationFormMethod = Exclude<FormMethod, "get">;
81
+
82
+ /**
83
+ * In v7, active navigation/fetcher form methods are exposed in uppercase on the
84
+ * RouterState. This is to align with the normalization done via fetch().
85
+ */
86
+ export type V7_FormMethod = UpperCaseFormMethod;
87
+ export type V7_MutationFormMethod = Exclude<V7_FormMethod, "GET">;
68
88
 
69
89
  export type FormEncType =
70
90
  | "application/x-www-form-urlencoded"
@@ -76,7 +96,7 @@ export type FormEncType =
76
96
  * external consumption
77
97
  */
78
98
  export interface Submission {
79
- formMethod: FormMethod;
99
+ formMethod: FormMethod | V7_FormMethod;
80
100
  formAction: string;
81
101
  formEncType: FormEncType;
82
102
  formData: FormData;