@remix-run/router 1.0.5 → 1.1.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/CHANGELOG.md CHANGED
@@ -1,5 +1,60 @@
1
1
  # `@remix-run/router`
2
2
 
3
+ ## 1.1.0-pre.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Fix issue with deeply nested optional segments ([#9727](https://github.com/remix-run/react-router/pull/9727))
8
+
9
+ ## 1.1.0-pre.0
10
+
11
+ ### Minor Changes
12
+
13
+ - Support for optional path segments ([#9650](https://github.com/remix-run/react-router/pull/9650))
14
+ - You can now denote optional path segments with a `?` as the last character in a path segment
15
+ - Optional params examples
16
+ - `:lang?/about` will get expanded and match:
17
+ - `/:lang/about`
18
+ - `/about`
19
+ - `/multistep/:widget1?/widget2?/widget3?` will get expanded and match:
20
+ - `/multistep/:widget1/:widget2/:widget3`
21
+ - `/multistep/:widget1/:widget2`
22
+ - `/multistep/:widget1`
23
+ - `/multistep`
24
+ - Optional static segment example
25
+ - `/fr?/about` will get expanded and match:
26
+ - `/fr/about`
27
+ - `/about`
28
+
29
+ ### Patch Changes
30
+
31
+ - Stop incorrectly matching on partial named parameters, i.e. `<Route path="prefix-:param">`, to align with how splat parameters work. If you were previously relying on this behavior then it's recommended to extract the static portion of the path at the `useParams` call site: ([#9506](https://github.com/remix-run/react-router/pull/9506))
32
+
33
+ ```jsx
34
+ // Old behavior at URL /prefix-123
35
+ <Route path="prefix-:id" element={<Comp /> }>
36
+
37
+ function Comp() {
38
+ let params = useParams(); // { id: '123' }
39
+ let id = params.id; // "123"
40
+ ...
41
+ }
42
+
43
+ // New behavior at URL /prefix-123
44
+ <Route path=":id" element={<Comp /> }>
45
+
46
+ function Comp() {
47
+ let params = useParams(); // { id: 'prefix-123' }
48
+ let id = params.id.replace(/^prefix-/, ''); // "123"
49
+ ...
50
+ }
51
+ ```
52
+
53
+ - Fix requests sent to revalidating loaders so they reflect a GET request ([#9660](https://github.com/remix-run/react-router/pull/9660))
54
+ - Persist `headers` on `loader` `request`'s after SSR document `action` request ([#9721](https://github.com/remix-run/react-router/pull/9721))
55
+ - `GET` forms now expose a submission on the loading navigation ([#9695](https://github.com/remix-run/react-router/pull/9695))
56
+ - Fix error boundary tracking for multiple errors bubbling to the same boundary ([#9702](https://github.com/remix-run/react-router/pull/9702))
57
+
3
58
  ## 1.0.5
4
59
 
5
60
  ### Patch Changes
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @remix-run/router v1.0.5
2
+ * @remix-run/router v1.1.0-pre.1
3
3
  *
4
4
  * Copyright (c) Remix Software Inc.
5
5
  *
@@ -611,9 +611,9 @@ function flattenRoutes(routes, branches, parentsMeta, parentPath) {
611
611
  parentPath = "";
612
612
  }
613
613
 
614
- routes.forEach((route, index) => {
614
+ let flattenRoute = (route, index, relativePath) => {
615
615
  let meta = {
616
- relativePath: route.path || "",
616
+ relativePath: relativePath === undefined ? route.path || "" : relativePath,
617
617
  caseSensitive: route.caseSensitive === true,
618
618
  childrenIndex: index,
619
619
  route
@@ -647,9 +647,71 @@ function flattenRoutes(routes, branches, parentsMeta, parentPath) {
647
647
  score: computeScore(path, route.index),
648
648
  routesMeta
649
649
  });
650
+ };
651
+
652
+ routes.forEach((route, index) => {
653
+ var _route$path;
654
+
655
+ // coarse-grain check for optional params
656
+ if (route.path === "" || !((_route$path = route.path) != null && _route$path.includes("?"))) {
657
+ flattenRoute(route, index);
658
+ } else {
659
+ for (let exploded of explodeOptionalSegments(route.path)) {
660
+ flattenRoute(route, index, exploded);
661
+ }
662
+ }
650
663
  });
651
664
  return branches;
652
665
  }
666
+ /**
667
+ * Computes all combinations of optional path segments for a given path,
668
+ * excluding combinations that are ambiguous and of lower priority.
669
+ *
670
+ * For example, `/one/:two?/three/:four?/:five?` explodes to:
671
+ * - `/one/three`
672
+ * - `/one/:two/three`
673
+ * - `/one/three/:four`
674
+ * - `/one/three/:five`
675
+ * - `/one/:two/three/:four`
676
+ * - `/one/:two/three/:five`
677
+ * - `/one/three/:four/:five`
678
+ * - `/one/:two/three/:four/:five`
679
+ */
680
+
681
+
682
+ function explodeOptionalSegments(path) {
683
+ let segments = path.split("/");
684
+ if (segments.length === 0) return [];
685
+ let [first, ...rest] = segments; // Optional path segments are denoted by a trailing `?`
686
+
687
+ let isOptional = first.endsWith("?"); // Compute the corresponding required segment: `foo?` -> `foo`
688
+
689
+ let required = first.replace(/\?$/, "");
690
+
691
+ if (rest.length === 0) {
692
+ // Intepret empty string as omitting an optional segment
693
+ // `["one", "", "three"]` corresponds to omitting `:two` from `/one/:two?/three` -> `/one/three`
694
+ return isOptional ? [required, ""] : [required];
695
+ }
696
+
697
+ let restExploded = explodeOptionalSegments(rest.join("/"));
698
+ let result = []; // All child paths with the prefix. Do this for all children before the
699
+ // optional version for all children so we get consistent ordering where the
700
+ // parent optional aspect is preferred as required. Otherwise, we can get
701
+ // child sections interspersed where deeper optional segments are higher than
702
+ // parent optional segments, where for example, /:two would explodes _earlier_
703
+ // then /:one. By always including the parent as required _for all children_
704
+ // first, we avoid this issue
705
+
706
+ result.push(...restExploded.map(subpath => subpath === "" ? required : [required, subpath].join("/"))); // Then if this is an optional value, add all child versions without
707
+
708
+ if (isOptional) {
709
+ result.push(...restExploded);
710
+ } // for absolute paths, ensure `/` instead of empty segment
711
+
712
+
713
+ return result.map(exploded => path.startsWith("/") && exploded === "" ? "/" : exploded);
714
+ }
653
715
 
654
716
  function rankRouteBranches(branches) {
655
717
  branches.sort((a, b) => a.score !== b.score ? b.score - a.score // Higher score first
@@ -733,14 +795,24 @@ function matchRouteBranch(branch, pathname) {
733
795
  */
734
796
 
735
797
 
736
- function generatePath(path, params) {
798
+ function generatePath(originalPath, params) {
737
799
  if (params === void 0) {
738
800
  params = {};
739
801
  }
740
802
 
741
- return path.replace(/:(\w+)/g, (_, key) => {
803
+ let path = originalPath;
804
+
805
+ if (path.endsWith("*") && path !== "*" && !path.endsWith("/*")) {
806
+ warning(false, "Route path \"" + path + "\" will be treated as if it were " + ("\"" + path.replace(/\*$/, "/*") + "\" because the `*` character must ") + "always follow a `/` in the pattern. To get rid of this warning, " + ("please change the route path to \"" + path.replace(/\*$/, "/*") + "\"."));
807
+ path = path.replace(/\*$/, "/*");
808
+ }
809
+
810
+ return path.replace(/^:(\w+)/g, (_, key) => {
742
811
  invariant(params[key] != null, "Missing \":" + key + "\" param");
743
812
  return params[key];
813
+ }).replace(/\/:(\w+)/g, (_, key) => {
814
+ invariant(params[key] != null, "Missing \":" + key + "\" param");
815
+ return "/" + params[key];
744
816
  }).replace(/(\/?)\*/, (_, prefix, __, str) => {
745
817
  const star = "*";
746
818
 
@@ -812,9 +884,9 @@ function compilePath(path, caseSensitive, end) {
812
884
  let regexpSource = "^" + path.replace(/\/*\*?$/, "") // Ignore trailing / and /*, we'll handle it below
813
885
  .replace(/^\/*/, "/") // Make sure it has a leading /
814
886
  .replace(/[\\.*+^$?{}|()[\]]/g, "\\$&") // Escape special regex chars
815
- .replace(/:(\w+)/g, (_, paramName) => {
887
+ .replace(/\/:(\w+)/g, (_, paramName) => {
816
888
  paramNames.push(paramName);
817
- return "([^\\/]+)";
889
+ return "/([^\\/]+)";
818
890
  });
819
891
 
820
892
  if (path.endsWith("*")) {
@@ -1295,9 +1367,9 @@ function isRouteErrorResponse(e) {
1295
1367
  * A Router instance manages all navigation and data loading/mutations
1296
1368
  */
1297
1369
 
1298
- const validActionMethodsArr = ["post", "put", "patch", "delete"];
1299
- const validActionMethods = new Set(validActionMethodsArr);
1300
- const validRequestMethodsArr = ["get", ...validActionMethodsArr];
1370
+ const validMutationMethodsArr = ["post", "put", "patch", "delete"];
1371
+ const validMutationMethods = new Set(validMutationMethodsArr);
1372
+ const validRequestMethodsArr = ["get", ...validMutationMethodsArr];
1301
1373
  const validRequestMethods = new Set(validRequestMethodsArr);
1302
1374
  const redirectStatusCodes = new Set([301, 302, 303, 307, 308]);
1303
1375
  const redirectPreserveMethodStatusCodes = new Set([307, 308]);
@@ -1534,7 +1606,7 @@ function createRouter(init) {
1534
1606
  // without having to touch history
1535
1607
 
1536
1608
  location = _extends({}, location, init.history.encodeLocation(location));
1537
- let historyAction = (opts && opts.replace) === true || submission != null ? exports.Action.Replace : exports.Action.Push;
1609
+ let historyAction = (opts && opts.replace) === true || submission != null && isMutationMethod(submission.formMethod) ? exports.Action.Replace : exports.Action.Push;
1538
1610
  let preventScrollReset = opts && "preventScrollReset" in opts ? opts.preventScrollReset === true : undefined;
1539
1611
  return await startNavigation(historyAction, location, {
1540
1612
  submission,
@@ -1638,7 +1710,7 @@ function createRouter(init) {
1638
1710
  pendingError = {
1639
1711
  [findNearestBoundary(matches).route.id]: opts.pendingError
1640
1712
  };
1641
- } else if (opts && opts.submission) {
1713
+ } else if (opts && opts.submission && isMutationMethod(opts.submission.formMethod)) {
1642
1714
  // Call action if we received an action submission
1643
1715
  let actionOutput = await handleAction(request, location, opts.submission, matches, {
1644
1716
  replace: opts.replace
@@ -1765,14 +1837,15 @@ function createRouter(init) {
1765
1837
  let loadingNavigation = overrideNavigation;
1766
1838
 
1767
1839
  if (!loadingNavigation) {
1768
- let navigation = {
1840
+ let navigation = _extends({
1769
1841
  state: "loading",
1770
1842
  location,
1771
1843
  formMethod: undefined,
1772
1844
  formAction: undefined,
1773
1845
  formEncType: undefined,
1774
1846
  formData: undefined
1775
- };
1847
+ }, submission);
1848
+
1776
1849
  loadingNavigation = navigation;
1777
1850
  }
1778
1851
 
@@ -1907,7 +1980,7 @@ function createRouter(init) {
1907
1980
  } = normalizeNavigateOptions(href, opts, true);
1908
1981
  let match = getTargetMatch(matches, path);
1909
1982
 
1910
- if (submission) {
1983
+ if (submission && isMutationMethod(submission.formMethod)) {
1911
1984
  handleFetcherAction(key, routeId, path, match, matches, submission);
1912
1985
  return;
1913
1986
  } // Store off the match so we can call it's shouldRevalidate on subsequent
@@ -1915,7 +1988,7 @@ function createRouter(init) {
1915
1988
 
1916
1989
 
1917
1990
  fetchLoadMatches.set(key, [path, match, matches]);
1918
- handleFetcherLoader(key, routeId, path, match, matches);
1991
+ handleFetcherLoader(key, routeId, path, match, matches, submission);
1919
1992
  } // Call the action for the matched fetcher.submit(), and then handle redirects,
1920
1993
  // errors, and revalidation
1921
1994
 
@@ -2096,17 +2169,19 @@ function createRouter(init) {
2096
2169
  } // Call the matched loader for fetcher.load(), handling redirects, errors, etc.
2097
2170
 
2098
2171
 
2099
- async function handleFetcherLoader(key, routeId, path, match, matches) {
2172
+ async function handleFetcherLoader(key, routeId, path, match, matches, submission) {
2100
2173
  let existingFetcher = state.fetchers.get(key); // Put this fetcher into it's loading state
2101
2174
 
2102
- let loadingFetcher = {
2175
+ let loadingFetcher = _extends({
2103
2176
  state: "loading",
2104
2177
  formMethod: undefined,
2105
2178
  formAction: undefined,
2106
2179
  formEncType: undefined,
2107
- formData: undefined,
2180
+ formData: undefined
2181
+ }, submission, {
2108
2182
  data: existingFetcher && existingFetcher.data
2109
- };
2183
+ });
2184
+
2110
2185
  state.fetchers.set(key, loadingFetcher);
2111
2186
  updateState({
2112
2187
  fetchers: new Map(state.fetchers)
@@ -2226,10 +2301,10 @@ function createRouter(init) {
2226
2301
  formEncType,
2227
2302
  formData
2228
2303
  } = state.navigation; // If this was a 307/308 submission we want to preserve the HTTP method and
2229
- // re-submit the POST/PUT/PATCH/DELETE as a submission navigation to the
2304
+ // re-submit the GET/POST/PUT/PATCH/DELETE as a submission navigation to the
2230
2305
  // redirected location
2231
2306
 
2232
- if (redirectPreserveMethodStatusCodes.has(redirect.status) && formMethod && isSubmissionMethod(formMethod) && formEncType && formData) {
2307
+ if (redirectPreserveMethodStatusCodes.has(redirect.status) && formMethod && isMutationMethod(formMethod) && formEncType && formData) {
2233
2308
  await startNavigation(redirectHistoryAction, redirectLocation, {
2234
2309
  submission: {
2235
2310
  formMethod,
@@ -2640,7 +2715,7 @@ function unstable_createStaticHandler(routes, opts) {
2640
2715
  invariant(request.signal, "query()/queryRoute() requests must contain an AbortController signal");
2641
2716
 
2642
2717
  try {
2643
- if (isSubmissionMethod(request.method.toLowerCase())) {
2718
+ if (isMutationMethod(request.method.toLowerCase())) {
2644
2719
  let result = await submit(request, matches, routeMatch || getTargetMatch(matches, location), requestContext, routeMatch != null);
2645
2720
  return result;
2646
2721
  }
@@ -2757,6 +2832,8 @@ function unstable_createStaticHandler(routes, opts) {
2757
2832
 
2758
2833
 
2759
2834
  let loaderRequest = new Request(request.url, {
2835
+ headers: request.headers,
2836
+ redirect: request.redirect,
2760
2837
  signal: request.signal
2761
2838
  });
2762
2839
  let context = await loadRouteData(loaderRequest, matches, requestContext);
@@ -2872,16 +2949,22 @@ function normalizeNavigateOptions(to, opts, isFetcher) {
2872
2949
  } // Create a Submission on non-GET navigations
2873
2950
 
2874
2951
 
2875
- if (opts.formMethod && isSubmissionMethod(opts.formMethod)) {
2876
- return {
2877
- path,
2878
- submission: {
2879
- formMethod: opts.formMethod,
2880
- formAction: stripHashFromPath(path),
2881
- formEncType: opts && opts.formEncType || "application/x-www-form-urlencoded",
2882
- formData: opts.formData
2883
- }
2952
+ let submission;
2953
+
2954
+ if (opts.formData) {
2955
+ submission = {
2956
+ formMethod: opts.formMethod || "get",
2957
+ formAction: stripHashFromPath(path),
2958
+ formEncType: opts && opts.formEncType || "application/x-www-form-urlencoded",
2959
+ formData: opts.formData
2884
2960
  };
2961
+
2962
+ if (isMutationMethod(submission.formMethod)) {
2963
+ return {
2964
+ path,
2965
+ submission
2966
+ };
2967
+ }
2885
2968
  } // Flatten submission onto URLSearchParams for GET submissions
2886
2969
 
2887
2970
 
@@ -2905,7 +2988,8 @@ function normalizeNavigateOptions(to, opts, isFetcher) {
2905
2988
  }
2906
2989
 
2907
2990
  return {
2908
- path: createPath(parsedPath)
2991
+ path: createPath(parsedPath),
2992
+ submission
2909
2993
  };
2910
2994
  } // Filter out all routes below any caught error as they aren't going to
2911
2995
  // render so we don't need to load them
@@ -3149,7 +3233,7 @@ function createClientSideRequest(location, signal, submission) {
3149
3233
  signal
3150
3234
  };
3151
3235
 
3152
- if (submission) {
3236
+ if (submission && isMutationMethod(submission.formMethod)) {
3153
3237
  let {
3154
3238
  formMethod,
3155
3239
  formEncType,
@@ -3199,11 +3283,14 @@ function processRouteLoaderData(matches, matchesToLoad, results, pendingError, a
3199
3283
  pendingError = undefined;
3200
3284
  }
3201
3285
 
3202
- errors = Object.assign(errors || {}, {
3203
- [boundaryMatch.route.id]: error
3204
- }); // Once we find our first (highest) error, we set the status code and
3286
+ errors = errors || {}; // Prefer higher error values if lower errors bubble to the same boundary
3287
+
3288
+ if (errors[boundaryMatch.route.id] == null) {
3289
+ errors[boundaryMatch.route.id] = error;
3290
+ } // Once we find our first (highest) error, we set the status code and
3205
3291
  // prevent deeper status codes from overriding
3206
3292
 
3293
+
3207
3294
  if (!foundError) {
3208
3295
  foundError = true;
3209
3296
  statusCode = isRouteErrorResponse(result.error) ? result.error.status : 500;
@@ -3419,8 +3506,8 @@ function isValidMethod(method) {
3419
3506
  return validRequestMethods.has(method);
3420
3507
  }
3421
3508
 
3422
- function isSubmissionMethod(method) {
3423
- return validActionMethods.has(method);
3509
+ function isMutationMethod(method) {
3510
+ return validMutationMethods.has(method);
3424
3511
  }
3425
3512
 
3426
3513
  async function resolveDeferredResults(currentMatches, matchesToLoad, results, signal, isFetcher, currentLoaderData) {