@remix-run/router 1.0.5-pre.2 → 1.1.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/CHANGELOG.md CHANGED
@@ -1,25 +1,63 @@
1
1
  # `@remix-run/router`
2
2
 
3
- ## 1.0.5-pre.2
3
+ ## 1.1.0-pre.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Support for optional path segments ([#9650](https://github.com/remix-run/react-router/pull/9650))
8
+ - You can now denote optional path segments with a `?` as the last character in a path segment
9
+ - Optional params examples
10
+ - `:lang?/about` will get expanded and match:
11
+ - `/:lang/about`
12
+ - `/about`
13
+ - `/multistep/:widget1?/widget2?/widget3?` will get expanded and match:
14
+ - `/multistep/:widget1/:widget2/:widget3`
15
+ - `/multistep/:widget1/:widget2`
16
+ - `/multistep/:widget1`
17
+ - `/multistep`
18
+ - Optional static segment example
19
+ - `/fr?/about` will get expanded and match:
20
+ - `/fr/about`
21
+ - `/about`
4
22
 
5
23
  ### Patch Changes
6
24
 
7
- - Add `requestContext` support to static handler `query`/`queryRoute` ([#9696](https://github.com/remix-run/react-router/pull/9696))
8
- - Note that the unstable API of `queryRoute(path, routeId)` has been changed to `queryRoute(path, { routeId, requestContext })`
25
+ - 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))
9
26
 
10
- ## 1.0.5-pre.1
27
+ ```jsx
28
+ // Old behavior at URL /prefix-123
29
+ <Route path="prefix-:id" element={<Comp /> }>
11
30
 
12
- ### Patch Changes
31
+ function Comp() {
32
+ let params = useParams(); // { id: '123' }
33
+ let id = params.id; // "123"
34
+ ...
35
+ }
13
36
 
14
- - Remove `instanceof Response` checks in favor of `isResponse` ([#9690](https://github.com/remix-run/react-router/pull/9690))
15
- - Fix URL creation in Remix integration tests ([#9689](https://github.com/remix-run/react-router/pull/9689))
37
+ // New behavior at URL /prefix-123
38
+ <Route path=":id" element={<Comp /> }>
39
+
40
+ function Comp() {
41
+ let params = useParams(); // { id: 'prefix-123' }
42
+ let id = params.id.replace(/^prefix-/, ''); // "123"
43
+ ...
44
+ }
45
+ ```
16
46
 
17
- ## 1.0.5-pre.0
47
+ - Fix requests sent to revalidating loaders so they reflect a GET request ([#9660](https://github.com/remix-run/react-router/pull/9660))
48
+ - Persist `headers` on `loader` `request`'s after SSR document `action` request ([#9721](https://github.com/remix-run/react-router/pull/9721))
49
+ - `GET` forms now expose a submission on the loading navigation ([#9695](https://github.com/remix-run/react-router/pull/9695))
50
+ - Fix error boundary tracking for multiple errors bubbling to the same boundary ([#9702](https://github.com/remix-run/react-router/pull/9702))
51
+
52
+ ## 1.0.5
18
53
 
19
54
  ### Patch Changes
20
55
 
21
- - Fix URL creation in Cloudflare Pages or other non-browser-environment ([#9682](https://github.com/remix-run/react-router/pull/9682))
22
- - Fix requests sent to revalidating loaders so they reflect a GET request ([#9680](https://github.com/remix-run/react-router/pull/9680))
56
+ - Fix requests sent to revalidating loaders so they reflect a `GET` request ([#9680](https://github.com/remix-run/react-router/pull/9680))
57
+ - Remove `instanceof Response` checks in favor of `isResponse` ([#9690](https://github.com/remix-run/react-router/pull/9690))
58
+ - Fix `URL` creation in Cloudflare Pages or other non-browser-environments ([#9682](https://github.com/remix-run/react-router/pull/9682), [#9689](https://github.com/remix-run/react-router/pull/9689))
59
+ - Add `requestContext` support to static handler `query`/`queryRoute` ([#9696](https://github.com/remix-run/react-router/pull/9696))
60
+ - Note that the unstable API of `queryRoute(path, routeId)` has been changed to `queryRoute(path, { routeId, requestContext })`
23
61
 
24
62
  ## 1.0.4
25
63
 
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @remix-run/router v1.0.5-pre.2
2
+ * @remix-run/router v1.1.0-pre.0
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,67 @@ 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
+ return restExploded.flatMap(subpath => {
699
+ // /one + / + :two/three -> /one/:two/three
700
+ let requiredExploded = subpath === "" ? required : required + "/" + subpath; // For optional segments, return the exploded path _without_ current segment first (`subpath`)
701
+ // and exploded path _with_ current segment later (`subpath`)
702
+ // This ensures that exploded paths are emitted in priority order
703
+ // `/one/three/:four` will come before `/one/three/:five`
704
+
705
+ return isOptional ? [subpath, requiredExploded] : [requiredExploded];
706
+ }).map(exploded => {
707
+ // for absolute paths, ensure `/` instead of empty segment
708
+ return path.startsWith("/") && exploded === "" ? "/" : exploded;
709
+ });
710
+ }
653
711
 
654
712
  function rankRouteBranches(branches) {
655
713
  branches.sort((a, b) => a.score !== b.score ? b.score - a.score // Higher score first
@@ -733,14 +791,24 @@ function matchRouteBranch(branch, pathname) {
733
791
  */
734
792
 
735
793
 
736
- function generatePath(path, params) {
794
+ function generatePath(originalPath, params) {
737
795
  if (params === void 0) {
738
796
  params = {};
739
797
  }
740
798
 
741
- return path.replace(/:(\w+)/g, (_, key) => {
799
+ let path = originalPath;
800
+
801
+ if (path.endsWith("*") && path !== "*" && !path.endsWith("/*")) {
802
+ 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(/\*$/, "/*") + "\"."));
803
+ path = path.replace(/\*$/, "/*");
804
+ }
805
+
806
+ return path.replace(/^:(\w+)/g, (_, key) => {
742
807
  invariant(params[key] != null, "Missing \":" + key + "\" param");
743
808
  return params[key];
809
+ }).replace(/\/:(\w+)/g, (_, key) => {
810
+ invariant(params[key] != null, "Missing \":" + key + "\" param");
811
+ return "/" + params[key];
744
812
  }).replace(/(\/?)\*/, (_, prefix, __, str) => {
745
813
  const star = "*";
746
814
 
@@ -812,9 +880,9 @@ function compilePath(path, caseSensitive, end) {
812
880
  let regexpSource = "^" + path.replace(/\/*\*?$/, "") // Ignore trailing / and /*, we'll handle it below
813
881
  .replace(/^\/*/, "/") // Make sure it has a leading /
814
882
  .replace(/[\\.*+^$?{}|()[\]]/g, "\\$&") // Escape special regex chars
815
- .replace(/:(\w+)/g, (_, paramName) => {
883
+ .replace(/\/:(\w+)/g, (_, paramName) => {
816
884
  paramNames.push(paramName);
817
- return "([^\\/]+)";
885
+ return "/([^\\/]+)";
818
886
  });
819
887
 
820
888
  if (path.endsWith("*")) {
@@ -1295,9 +1363,9 @@ function isRouteErrorResponse(e) {
1295
1363
  * A Router instance manages all navigation and data loading/mutations
1296
1364
  */
1297
1365
 
1298
- const validActionMethodsArr = ["post", "put", "patch", "delete"];
1299
- const validActionMethods = new Set(validActionMethodsArr);
1300
- const validRequestMethodsArr = ["get", ...validActionMethodsArr];
1366
+ const validMutationMethodsArr = ["post", "put", "patch", "delete"];
1367
+ const validMutationMethods = new Set(validMutationMethodsArr);
1368
+ const validRequestMethodsArr = ["get", ...validMutationMethodsArr];
1301
1369
  const validRequestMethods = new Set(validRequestMethodsArr);
1302
1370
  const redirectStatusCodes = new Set([301, 302, 303, 307, 308]);
1303
1371
  const redirectPreserveMethodStatusCodes = new Set([307, 308]);
@@ -1534,7 +1602,7 @@ function createRouter(init) {
1534
1602
  // without having to touch history
1535
1603
 
1536
1604
  location = _extends({}, location, init.history.encodeLocation(location));
1537
- let historyAction = (opts && opts.replace) === true || submission != null ? exports.Action.Replace : exports.Action.Push;
1605
+ let historyAction = (opts && opts.replace) === true || submission != null && isMutationMethod(submission.formMethod) ? exports.Action.Replace : exports.Action.Push;
1538
1606
  let preventScrollReset = opts && "preventScrollReset" in opts ? opts.preventScrollReset === true : undefined;
1539
1607
  return await startNavigation(historyAction, location, {
1540
1608
  submission,
@@ -1638,7 +1706,7 @@ function createRouter(init) {
1638
1706
  pendingError = {
1639
1707
  [findNearestBoundary(matches).route.id]: opts.pendingError
1640
1708
  };
1641
- } else if (opts && opts.submission) {
1709
+ } else if (opts && opts.submission && isMutationMethod(opts.submission.formMethod)) {
1642
1710
  // Call action if we received an action submission
1643
1711
  let actionOutput = await handleAction(request, location, opts.submission, matches, {
1644
1712
  replace: opts.replace
@@ -1765,14 +1833,15 @@ function createRouter(init) {
1765
1833
  let loadingNavigation = overrideNavigation;
1766
1834
 
1767
1835
  if (!loadingNavigation) {
1768
- let navigation = {
1836
+ let navigation = _extends({
1769
1837
  state: "loading",
1770
1838
  location,
1771
1839
  formMethod: undefined,
1772
1840
  formAction: undefined,
1773
1841
  formEncType: undefined,
1774
1842
  formData: undefined
1775
- };
1843
+ }, submission);
1844
+
1776
1845
  loadingNavigation = navigation;
1777
1846
  }
1778
1847
 
@@ -1907,7 +1976,7 @@ function createRouter(init) {
1907
1976
  } = normalizeNavigateOptions(href, opts, true);
1908
1977
  let match = getTargetMatch(matches, path);
1909
1978
 
1910
- if (submission) {
1979
+ if (submission && isMutationMethod(submission.formMethod)) {
1911
1980
  handleFetcherAction(key, routeId, path, match, matches, submission);
1912
1981
  return;
1913
1982
  } // Store off the match so we can call it's shouldRevalidate on subsequent
@@ -1915,7 +1984,7 @@ function createRouter(init) {
1915
1984
 
1916
1985
 
1917
1986
  fetchLoadMatches.set(key, [path, match, matches]);
1918
- handleFetcherLoader(key, routeId, path, match, matches);
1987
+ handleFetcherLoader(key, routeId, path, match, matches, submission);
1919
1988
  } // Call the action for the matched fetcher.submit(), and then handle redirects,
1920
1989
  // errors, and revalidation
1921
1990
 
@@ -2096,17 +2165,19 @@ function createRouter(init) {
2096
2165
  } // Call the matched loader for fetcher.load(), handling redirects, errors, etc.
2097
2166
 
2098
2167
 
2099
- async function handleFetcherLoader(key, routeId, path, match, matches) {
2168
+ async function handleFetcherLoader(key, routeId, path, match, matches, submission) {
2100
2169
  let existingFetcher = state.fetchers.get(key); // Put this fetcher into it's loading state
2101
2170
 
2102
- let loadingFetcher = {
2171
+ let loadingFetcher = _extends({
2103
2172
  state: "loading",
2104
2173
  formMethod: undefined,
2105
2174
  formAction: undefined,
2106
2175
  formEncType: undefined,
2107
- formData: undefined,
2176
+ formData: undefined
2177
+ }, submission, {
2108
2178
  data: existingFetcher && existingFetcher.data
2109
- };
2179
+ });
2180
+
2110
2181
  state.fetchers.set(key, loadingFetcher);
2111
2182
  updateState({
2112
2183
  fetchers: new Map(state.fetchers)
@@ -2226,10 +2297,10 @@ function createRouter(init) {
2226
2297
  formEncType,
2227
2298
  formData
2228
2299
  } = 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
2300
+ // re-submit the GET/POST/PUT/PATCH/DELETE as a submission navigation to the
2230
2301
  // redirected location
2231
2302
 
2232
- if (redirectPreserveMethodStatusCodes.has(redirect.status) && formMethod && isSubmissionMethod(formMethod) && formEncType && formData) {
2303
+ if (redirectPreserveMethodStatusCodes.has(redirect.status) && formMethod && isMutationMethod(formMethod) && formEncType && formData) {
2233
2304
  await startNavigation(redirectHistoryAction, redirectLocation, {
2234
2305
  submission: {
2235
2306
  formMethod,
@@ -2640,7 +2711,7 @@ function unstable_createStaticHandler(routes, opts) {
2640
2711
  invariant(request.signal, "query()/queryRoute() requests must contain an AbortController signal");
2641
2712
 
2642
2713
  try {
2643
- if (isSubmissionMethod(request.method.toLowerCase())) {
2714
+ if (isMutationMethod(request.method.toLowerCase())) {
2644
2715
  let result = await submit(request, matches, routeMatch || getTargetMatch(matches, location), requestContext, routeMatch != null);
2645
2716
  return result;
2646
2717
  }
@@ -2757,6 +2828,8 @@ function unstable_createStaticHandler(routes, opts) {
2757
2828
 
2758
2829
 
2759
2830
  let loaderRequest = new Request(request.url, {
2831
+ headers: request.headers,
2832
+ redirect: request.redirect,
2760
2833
  signal: request.signal
2761
2834
  });
2762
2835
  let context = await loadRouteData(loaderRequest, matches, requestContext);
@@ -2872,16 +2945,22 @@ function normalizeNavigateOptions(to, opts, isFetcher) {
2872
2945
  } // Create a Submission on non-GET navigations
2873
2946
 
2874
2947
 
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
- }
2948
+ let submission;
2949
+
2950
+ if (opts.formData) {
2951
+ submission = {
2952
+ formMethod: opts.formMethod || "get",
2953
+ formAction: stripHashFromPath(path),
2954
+ formEncType: opts && opts.formEncType || "application/x-www-form-urlencoded",
2955
+ formData: opts.formData
2884
2956
  };
2957
+
2958
+ if (isMutationMethod(submission.formMethod)) {
2959
+ return {
2960
+ path,
2961
+ submission
2962
+ };
2963
+ }
2885
2964
  } // Flatten submission onto URLSearchParams for GET submissions
2886
2965
 
2887
2966
 
@@ -2905,7 +2984,8 @@ function normalizeNavigateOptions(to, opts, isFetcher) {
2905
2984
  }
2906
2985
 
2907
2986
  return {
2908
- path: createPath(parsedPath)
2987
+ path: createPath(parsedPath),
2988
+ submission
2909
2989
  };
2910
2990
  } // Filter out all routes below any caught error as they aren't going to
2911
2991
  // render so we don't need to load them
@@ -3149,7 +3229,7 @@ function createClientSideRequest(location, signal, submission) {
3149
3229
  signal
3150
3230
  };
3151
3231
 
3152
- if (submission) {
3232
+ if (submission && isMutationMethod(submission.formMethod)) {
3153
3233
  let {
3154
3234
  formMethod,
3155
3235
  formEncType,
@@ -3199,11 +3279,14 @@ function processRouteLoaderData(matches, matchesToLoad, results, pendingError, a
3199
3279
  pendingError = undefined;
3200
3280
  }
3201
3281
 
3202
- errors = Object.assign(errors || {}, {
3203
- [boundaryMatch.route.id]: error
3204
- }); // Once we find our first (highest) error, we set the status code and
3282
+ errors = errors || {}; // Prefer higher error values if lower errors bubble to the same boundary
3283
+
3284
+ if (errors[boundaryMatch.route.id] == null) {
3285
+ errors[boundaryMatch.route.id] = error;
3286
+ } // Once we find our first (highest) error, we set the status code and
3205
3287
  // prevent deeper status codes from overriding
3206
3288
 
3289
+
3207
3290
  if (!foundError) {
3208
3291
  foundError = true;
3209
3292
  statusCode = isRouteErrorResponse(result.error) ? result.error.status : 500;
@@ -3419,8 +3502,8 @@ function isValidMethod(method) {
3419
3502
  return validRequestMethods.has(method);
3420
3503
  }
3421
3504
 
3422
- function isSubmissionMethod(method) {
3423
- return validActionMethods.has(method);
3505
+ function isMutationMethod(method) {
3506
+ return validMutationMethods.has(method);
3424
3507
  }
3425
3508
 
3426
3509
  async function resolveDeferredResults(currentMatches, matchesToLoad, results, signal, isFetcher, currentLoaderData) {