@remix-run/router 0.0.0-experimental-35fa15e5 → 0.0.0-experimental-8bb3ffdf
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 +200 -0
- package/README.md +1 -1
- package/dist/router.cjs.js +124 -103
- package/dist/router.cjs.js.map +1 -1
- package/dist/router.d.ts +8 -0
- package/dist/router.js +115 -98
- package/dist/router.js.map +1 -1
- package/dist/router.umd.js +124 -103
- package/dist/router.umd.js.map +1 -1
- package/dist/router.umd.min.js +2 -2
- package/dist/router.umd.min.js.map +1 -1
- package/dist/utils.d.ts +1 -1
- package/package.json +1 -1
- package/router.ts +118 -78
- package/utils.ts +18 -30
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,205 @@
|
|
|
1
1
|
# `@remix-run/router`
|
|
2
2
|
|
|
3
|
+
## 1.14.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Added a new `future.v7_partialHydration` future flag that enables partial hydration of a data router when Server-Side Rendering. This allows you to provide `hydrationData.loaderData` that has values for _some_ initially matched route loaders, but not all. When this flag is enabled, the router will call `loader` functions for routes that do not have hydration loader data during `router.initialize()`, and it will render down to the deepest provided `HydrateFallback` (up to the first route without hydration data) while it executes the unhydrated routes. ([#11033](https://github.com/remix-run/react-router/pull/11033))
|
|
8
|
+
|
|
9
|
+
For example, the following router has a `root` and `index` route, but only provided `hydrationData.loaderData` for the `root` route. Because the `index` route has a `loader`, we need to run that during initialization. With `future.v7_partialHydration` specified, `<RouterProvider>` will render the `RootComponent` (because it has data) and then the `IndexFallback` (since it does not have data). Once `indexLoader` finishes, application will update and display `IndexComponent`.
|
|
10
|
+
|
|
11
|
+
```jsx
|
|
12
|
+
let router = createBrowserRouter(
|
|
13
|
+
[
|
|
14
|
+
{
|
|
15
|
+
id: "root",
|
|
16
|
+
path: "/",
|
|
17
|
+
loader: rootLoader,
|
|
18
|
+
Component: RootComponent,
|
|
19
|
+
Fallback: RootFallback,
|
|
20
|
+
children: [
|
|
21
|
+
{
|
|
22
|
+
id: "index",
|
|
23
|
+
index: true,
|
|
24
|
+
loader: indexLoader,
|
|
25
|
+
Component: IndexComponent,
|
|
26
|
+
HydrateFallback: IndexFallback,
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
{
|
|
32
|
+
future: {
|
|
33
|
+
v7_partialHydration: true,
|
|
34
|
+
},
|
|
35
|
+
hydrationData: {
|
|
36
|
+
loaderData: {
|
|
37
|
+
root: { message: "Hydrated from Root!" },
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
}
|
|
41
|
+
);
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
If the above example did not have an `IndexFallback`, then `RouterProvider` would instead render the `RootFallback` while it executed the `indexLoader`.
|
|
45
|
+
|
|
46
|
+
**Note:** When `future.v7_partialHydration` is provided, the `<RouterProvider fallbackElement>` prop is ignored since you can move it to a `Fallback` on your top-most route. The `fallbackElement` prop will be removed in React Router v7 when `v7_partialHydration` behavior becomes the standard behavior.
|
|
47
|
+
|
|
48
|
+
- Add a new `future.v7_relativeSplatPath` flag to implement a breaking bug fix to relative routing when inside a splat route. ([#11087](https://github.com/remix-run/react-router/pull/11087))
|
|
49
|
+
|
|
50
|
+
This fix was originally added in [#10983](https://github.com/remix-run/react-router/issues/10983) and was later reverted in [#11078](https://github.com/remix-run/react-router/pull/11078) because it was determined that a large number of existing applications were relying on the buggy behavior (see [#11052](https://github.com/remix-run/react-router/issues/11052))
|
|
51
|
+
|
|
52
|
+
**The Bug**
|
|
53
|
+
The buggy behavior is that without this flag, the default behavior when resolving relative paths is to _ignore_ any splat (`*`) portion of the current route path.
|
|
54
|
+
|
|
55
|
+
**The Background**
|
|
56
|
+
This decision was originally made thinking that it would make the concept of nested different sections of your apps in `<Routes>` easier if relative routing would _replace_ the current splat:
|
|
57
|
+
|
|
58
|
+
```jsx
|
|
59
|
+
<BrowserRouter>
|
|
60
|
+
<Routes>
|
|
61
|
+
<Route path="/" element={<Home />} />
|
|
62
|
+
<Route path="dashboard/*" element={<Dashboard />} />
|
|
63
|
+
</Routes>
|
|
64
|
+
</BrowserRouter>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Any paths like `/dashboard`, `/dashboard/team`, `/dashboard/projects` will match the `Dashboard` route. The dashboard component itself can then render nested `<Routes>`:
|
|
68
|
+
|
|
69
|
+
```jsx
|
|
70
|
+
function Dashboard() {
|
|
71
|
+
return (
|
|
72
|
+
<div>
|
|
73
|
+
<h2>Dashboard</h2>
|
|
74
|
+
<nav>
|
|
75
|
+
<Link to="/">Dashboard Home</Link>
|
|
76
|
+
<Link to="team">Team</Link>
|
|
77
|
+
<Link to="projects">Projects</Link>
|
|
78
|
+
</nav>
|
|
79
|
+
|
|
80
|
+
<Routes>
|
|
81
|
+
<Route path="/" element={<DashboardHome />} />
|
|
82
|
+
<Route path="team" element={<DashboardTeam />} />
|
|
83
|
+
<Route path="projects" element={<DashboardProjects />} />
|
|
84
|
+
</Routes>
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Now, all links and route paths are relative to the router above them. This makes code splitting and compartmentalizing your app really easy. You could render the `Dashboard` as its own independent app, or embed it into your large app without making any changes to it.
|
|
91
|
+
|
|
92
|
+
**The Problem**
|
|
93
|
+
|
|
94
|
+
The problem is that this concept of ignoring part of a path breaks a lot of other assumptions in React Router - namely that `"."` always means the current location pathname for that route. When we ignore the splat portion, we start getting invalid paths when using `"."`:
|
|
95
|
+
|
|
96
|
+
```jsx
|
|
97
|
+
// If we are on URL /dashboard/team, and we want to link to /dashboard/team:
|
|
98
|
+
function DashboardTeam() {
|
|
99
|
+
// ❌ This is broken and results in <a href="/dashboard">
|
|
100
|
+
return <Link to=".">A broken link to the Current URL</Link>;
|
|
101
|
+
|
|
102
|
+
// ✅ This is fixed but super unintuitive since we're already at /dashboard/team!
|
|
103
|
+
return <Link to="./team">A broken link to the Current URL</Link>;
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
We've also introduced an issue that we can no longer move our `DashboardTeam` component around our route hierarchy easily - since it behaves differently if we're underneath a non-splat route, such as `/dashboard/:widget`. Now, our `"."` links will, properly point to ourself _inclusive of the dynamic param value_ so behavior will break from it's corresponding usage in a `/dashboard/*` route.
|
|
108
|
+
|
|
109
|
+
Even worse, consider a nested splat route configuration:
|
|
110
|
+
|
|
111
|
+
```jsx
|
|
112
|
+
<BrowserRouter>
|
|
113
|
+
<Routes>
|
|
114
|
+
<Route path="dashboard">
|
|
115
|
+
<Route path="*" element={<Dashboard />} />
|
|
116
|
+
</Route>
|
|
117
|
+
</Routes>
|
|
118
|
+
</BrowserRouter>
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Now, a `<Link to=".">` and a `<Link to="..">` inside the `Dashboard` component go to the same place! That is definitely not correct!
|
|
122
|
+
|
|
123
|
+
Another common issue arose in Data Routers (and Remix) where any `<Form>` should post to it's own route `action` if you the user doesn't specify a form action:
|
|
124
|
+
|
|
125
|
+
```jsx
|
|
126
|
+
let router = createBrowserRouter({
|
|
127
|
+
path: "/dashboard",
|
|
128
|
+
children: [
|
|
129
|
+
{
|
|
130
|
+
path: "*",
|
|
131
|
+
action: dashboardAction,
|
|
132
|
+
Component() {
|
|
133
|
+
// ❌ This form is broken! It throws a 405 error when it submits because
|
|
134
|
+
// it tries to submit to /dashboard (without the splat value) and the parent
|
|
135
|
+
// `/dashboard` route doesn't have an action
|
|
136
|
+
return <Form method="post">...</Form>;
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
});
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
This is just a compounded issue from the above because the default location for a `Form` to submit to is itself (`"."`) - and if we ignore the splat portion, that now resolves to the parent route.
|
|
144
|
+
|
|
145
|
+
**The Solution**
|
|
146
|
+
If you are leveraging this behavior, it's recommended to enable the future flag, move your splat to it's own route, and leverage `../` for any links to "sibling" pages:
|
|
147
|
+
|
|
148
|
+
```jsx
|
|
149
|
+
<BrowserRouter>
|
|
150
|
+
<Routes>
|
|
151
|
+
<Route path="dashboard">
|
|
152
|
+
<Route path="*" element={<Dashboard />} />
|
|
153
|
+
</Route>
|
|
154
|
+
</Routes>
|
|
155
|
+
</BrowserRouter>
|
|
156
|
+
|
|
157
|
+
function Dashboard() {
|
|
158
|
+
return (
|
|
159
|
+
<div>
|
|
160
|
+
<h2>Dashboard</h2>
|
|
161
|
+
<nav>
|
|
162
|
+
<Link to="..">Dashboard Home</Link>
|
|
163
|
+
<Link to="../team">Team</Link>
|
|
164
|
+
<Link to="../projects">Projects</Link>
|
|
165
|
+
</nav>
|
|
166
|
+
|
|
167
|
+
<Routes>
|
|
168
|
+
<Route path="/" element={<DashboardHome />} />
|
|
169
|
+
<Route path="team" element={<DashboardTeam />} />
|
|
170
|
+
<Route path="projects" element={<DashboardProjects />} />
|
|
171
|
+
</Router>
|
|
172
|
+
</div>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
This way, `.` means "the full current pathname for my route" in all cases (including static, dynamic, and splat routes) and `..` always means "my parents pathname".
|
|
178
|
+
|
|
179
|
+
### Patch Changes
|
|
180
|
+
|
|
181
|
+
- Catch and bubble errors thrown when trying to unwrap responses from `loader`/`action` functions ([#11061](https://github.com/remix-run/react-router/pull/11061))
|
|
182
|
+
- Fix `relative="path"` issue when rendering `Link`/`NavLink` outside of matched routes ([#11062](https://github.com/remix-run/react-router/pull/11062))
|
|
183
|
+
|
|
184
|
+
## 1.13.1
|
|
185
|
+
|
|
186
|
+
### Patch Changes
|
|
187
|
+
|
|
188
|
+
- Revert the `useResolvedPath` fix for splat routes due to a large number of applications that were relying on the buggy behavior (see https://github.com/remix-run/react-router/issues/11052#issuecomment-1836589329). We plan to re-introduce this fix behind a future flag in the next minor version. ([#11078](https://github.com/remix-run/react-router/pull/11078))
|
|
189
|
+
|
|
190
|
+
## 1.13.0
|
|
191
|
+
|
|
192
|
+
### Minor Changes
|
|
193
|
+
|
|
194
|
+
- Export the `PathParam` type from the public API ([#10719](https://github.com/remix-run/react-router/pull/10719))
|
|
195
|
+
|
|
196
|
+
### Patch Changes
|
|
197
|
+
|
|
198
|
+
- Fix bug with `resolveTo` in splat routes ([#11045](https://github.com/remix-run/react-router/pull/11045))
|
|
199
|
+
- This is a follow up to [#10983](https://github.com/remix-run/react-router/pull/10983) to handle the few other code paths using `getPathContributingMatches`
|
|
200
|
+
- This removes the `UNSAFE_getPathContributingMatches` export from `@remix-run/router` since we no longer need this in the `react-router`/`react-router-dom` layers
|
|
201
|
+
- Do not revalidate unmounted fetchers when `v7_fetcherPersist` is enabled ([#11044](https://github.com/remix-run/react-router/pull/11044))
|
|
202
|
+
|
|
3
203
|
## 1.12.0
|
|
4
204
|
|
|
5
205
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ The `@remix-run/router` package is a framework-agnostic routing package (sometim
|
|
|
4
4
|
|
|
5
5
|
If you're using React Router, you should never `import` anything directly from the `@remix-run/router` - you should have everything you need in `react-router-dom` (or `react-router`/`react-router-native` if you're not rendering in the browser). All of those packages should re-export everything you would otherwise need from `@remix-run/router`.
|
|
6
6
|
|
|
7
|
-
>
|
|
7
|
+
> [!WARNING]
|
|
8
8
|
>
|
|
9
9
|
> This router is a low-level package intended to be consumed by UI layer routing libraries. You should very likely not be using this package directly unless you are authoring a routing library such as [`react-router-dom`][react-router-repo] or one of it's other [UI ports][remix-routers-repo].
|
|
10
10
|
|
package/dist/router.cjs.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @remix-run/router v0.0.0-experimental-
|
|
2
|
+
* @remix-run/router v0.0.0-experimental-8bb3ffdf
|
|
3
3
|
*
|
|
4
4
|
* Copyright (c) Remix Software Inc.
|
|
5
5
|
*
|
|
@@ -1194,9 +1194,16 @@ function getPathContributingMatches(matches) {
|
|
|
1194
1194
|
|
|
1195
1195
|
// Return the array of pathnames for the current route matches - used to
|
|
1196
1196
|
// generate the routePathnames input for resolveTo()
|
|
1197
|
-
function getResolveToMatches(matches) {
|
|
1198
|
-
|
|
1199
|
-
|
|
1197
|
+
function getResolveToMatches(matches, v7_relativeSplatPath) {
|
|
1198
|
+
let pathMatches = getPathContributingMatches(matches);
|
|
1199
|
+
|
|
1200
|
+
// When v7_relativeSplatPath is enabled, use the full pathname for the leaf
|
|
1201
|
+
// match so we include splat values for "." links. See:
|
|
1202
|
+
// https://github.com/remix-run/react-router/issues/11052#issuecomment-1836589329
|
|
1203
|
+
if (v7_relativeSplatPath) {
|
|
1204
|
+
return pathMatches.map((match, idx) => idx === matches.length - 1 ? match.pathname : match.pathnameBase);
|
|
1205
|
+
}
|
|
1206
|
+
return pathMatches.map(match => match.pathnameBase);
|
|
1200
1207
|
}
|
|
1201
1208
|
|
|
1202
1209
|
/**
|
|
@@ -1230,37 +1237,21 @@ function resolveTo(toArg, routePathnames, locationPathname, isPathRelative) {
|
|
|
1230
1237
|
// to the current location's pathname and *not* the route pathname.
|
|
1231
1238
|
if (toPathname == null) {
|
|
1232
1239
|
from = locationPathname;
|
|
1233
|
-
} else if (isPathRelative) {
|
|
1234
|
-
let fromSegments = routePathnames[routePathnames.length - 1].replace(/^\//, "").split("/");
|
|
1235
|
-
if (toPathname.startsWith("..")) {
|
|
1236
|
-
let toSegments = toPathname.split("/");
|
|
1237
|
-
|
|
1238
|
-
// With relative="path", each leading .. segment means "go up one URL segment"
|
|
1239
|
-
while (toSegments[0] === "..") {
|
|
1240
|
-
toSegments.shift();
|
|
1241
|
-
fromSegments.pop();
|
|
1242
|
-
}
|
|
1243
|
-
to.pathname = toSegments.join("/");
|
|
1244
|
-
}
|
|
1245
|
-
from = "/" + fromSegments.join("/");
|
|
1246
1240
|
} else {
|
|
1247
1241
|
let routePathnameIndex = routePathnames.length - 1;
|
|
1248
|
-
if (toPathname.startsWith("..")) {
|
|
1249
|
-
let toSegments = toPathname.split("/");
|
|
1250
1242
|
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1243
|
+
// With relative="route" (the default), each leading .. segment means
|
|
1244
|
+
// "go up one route" instead of "go up one URL segment". This is a key
|
|
1245
|
+
// difference from how <a href> works and a major reason we call this a
|
|
1246
|
+
// "to" value instead of a "href".
|
|
1247
|
+
if (!isPathRelative && toPathname.startsWith("..")) {
|
|
1248
|
+
let toSegments = toPathname.split("/");
|
|
1255
1249
|
while (toSegments[0] === "..") {
|
|
1256
1250
|
toSegments.shift();
|
|
1257
1251
|
routePathnameIndex -= 1;
|
|
1258
1252
|
}
|
|
1259
1253
|
to.pathname = toSegments.join("/");
|
|
1260
1254
|
}
|
|
1261
|
-
|
|
1262
|
-
// If there are more ".." segments than parent routes, resolve relative to
|
|
1263
|
-
// the root / URL.
|
|
1264
1255
|
from = routePathnameIndex >= 0 ? routePathnames[routePathnameIndex] : "/";
|
|
1265
1256
|
}
|
|
1266
1257
|
let path = resolvePath(to, from);
|
|
@@ -1687,7 +1678,8 @@ function createRouter(init) {
|
|
|
1687
1678
|
v7_fetcherPersist: false,
|
|
1688
1679
|
v7_normalizeFormMethod: false,
|
|
1689
1680
|
v7_partialHydration: false,
|
|
1690
|
-
v7_prependBasename: false
|
|
1681
|
+
v7_prependBasename: false,
|
|
1682
|
+
v7_relativeSplatPath: false
|
|
1691
1683
|
}, init.future);
|
|
1692
1684
|
// Cleanup function for history
|
|
1693
1685
|
let unlistenHistory = null;
|
|
@@ -1723,18 +1715,28 @@ function createRouter(init) {
|
|
|
1723
1715
|
[route.id]: error
|
|
1724
1716
|
};
|
|
1725
1717
|
}
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1718
|
+
let initialized;
|
|
1719
|
+
let hasLazyRoutes = initialMatches.some(m => m.route.lazy);
|
|
1720
|
+
let hasLoaders = initialMatches.some(m => m.route.loader);
|
|
1721
|
+
if (hasLazyRoutes) {
|
|
1722
|
+
// All initialMatches need to be loaded before we're ready. If we have lazy
|
|
1723
|
+
// functions around still then we'll need to run them in initialize()
|
|
1724
|
+
initialized = false;
|
|
1725
|
+
} else if (!hasLoaders) {
|
|
1726
|
+
// If we've got no loaders to run, then we're good to go
|
|
1727
|
+
initialized = true;
|
|
1728
|
+
} else if (future.v7_partialHydration) {
|
|
1729
|
+
// If partial hydration is enabled, we're initialized so long as we were
|
|
1730
|
+
// provided with hydrationData for every route with a loader, and no loaders
|
|
1731
|
+
// were marked for explicit hydration
|
|
1732
|
+
let loaderData = init.hydrationData ? init.hydrationData.loaderData : null;
|
|
1733
|
+
let errors = init.hydrationData ? init.hydrationData.errors : null;
|
|
1734
|
+
initialized = initialMatches.every(m => m.route.loader && m.route.loader.hydrate !== true && (loaderData && loaderData[m.route.id] !== undefined || errors && errors[m.route.id] !== undefined));
|
|
1735
|
+
} else {
|
|
1736
|
+
// Without partial hydration - we're initialized if we were provided any
|
|
1737
|
+
// hydrationData - which is expected to be complete
|
|
1738
|
+
initialized = init.hydrationData != null;
|
|
1739
|
+
}
|
|
1738
1740
|
let router;
|
|
1739
1741
|
let state = {
|
|
1740
1742
|
historyAction: init.history.action,
|
|
@@ -1901,7 +1903,7 @@ function createRouter(init) {
|
|
|
1901
1903
|
// in the normal navigation flow. For SSR it's expected that lazy modules are
|
|
1902
1904
|
// resolved prior to router creation since we can't go into a fallbackElement
|
|
1903
1905
|
// UI for SSR'd apps
|
|
1904
|
-
if (!state.initialized
|
|
1906
|
+
if (!state.initialized) {
|
|
1905
1907
|
startNavigation(Action.Pop, state.location, {
|
|
1906
1908
|
initialHydration: true
|
|
1907
1909
|
});
|
|
@@ -2093,7 +2095,7 @@ function createRouter(init) {
|
|
|
2093
2095
|
init.history.go(to);
|
|
2094
2096
|
return;
|
|
2095
2097
|
}
|
|
2096
|
-
let normalizedPath = normalizeTo(state.location, state.matches, basename, future.v7_prependBasename, to, opts == null ? void 0 : opts.fromRouteId, opts == null ? void 0 : opts.relative);
|
|
2098
|
+
let normalizedPath = normalizeTo(state.location, state.matches, basename, future.v7_prependBasename, to, future.v7_relativeSplatPath, opts == null ? void 0 : opts.fromRouteId, opts == null ? void 0 : opts.relative);
|
|
2097
2099
|
let {
|
|
2098
2100
|
path,
|
|
2099
2101
|
submission,
|
|
@@ -2342,7 +2344,7 @@ function createRouter(init) {
|
|
|
2342
2344
|
})
|
|
2343
2345
|
};
|
|
2344
2346
|
} else {
|
|
2345
|
-
result = await callLoaderOrAction("action", request, actionMatch, matches, manifest, mapRouteProperties, basename);
|
|
2347
|
+
result = await callLoaderOrAction("action", request, actionMatch, matches, manifest, mapRouteProperties, basename, future.v7_relativeSplatPath);
|
|
2346
2348
|
if (request.signal.aborted) {
|
|
2347
2349
|
return {
|
|
2348
2350
|
shortCircuited: true
|
|
@@ -2552,7 +2554,7 @@ function createRouter(init) {
|
|
|
2552
2554
|
if (fetchControllers.has(key)) abortFetcher(key);
|
|
2553
2555
|
let flushSync = (opts && opts.unstable_flushSync) === true;
|
|
2554
2556
|
let routesToUse = inFlightDataRoutes || dataRoutes;
|
|
2555
|
-
let normalizedPath = normalizeTo(state.location, state.matches, basename, future.v7_prependBasename, href, routeId, opts == null ? void 0 : opts.relative);
|
|
2557
|
+
let normalizedPath = normalizeTo(state.location, state.matches, basename, future.v7_prependBasename, href, future.v7_relativeSplatPath, routeId, opts == null ? void 0 : opts.relative);
|
|
2556
2558
|
let matches = matchRoutes(routesToUse, normalizedPath, basename);
|
|
2557
2559
|
if (!matches) {
|
|
2558
2560
|
setFetcherError(key, routeId, getInternalRouterError(404, {
|
|
@@ -2617,7 +2619,7 @@ function createRouter(init) {
|
|
|
2617
2619
|
let fetchRequest = createClientSideRequest(init.history, path, abortController.signal, submission);
|
|
2618
2620
|
fetchControllers.set(key, abortController);
|
|
2619
2621
|
let originatingLoadId = incrementingLoadId;
|
|
2620
|
-
let actionResult = await callLoaderOrAction("action", fetchRequest, match, requestMatches, manifest, mapRouteProperties, basename);
|
|
2622
|
+
let actionResult = await callLoaderOrAction("action", fetchRequest, match, requestMatches, manifest, mapRouteProperties, basename, future.v7_relativeSplatPath);
|
|
2621
2623
|
if (fetchRequest.signal.aborted) {
|
|
2622
2624
|
// We can delete this so long as we weren't aborted by our own fetcher
|
|
2623
2625
|
// re-submit which would have put _new_ controller is in fetchControllers
|
|
@@ -2626,32 +2628,40 @@ function createRouter(init) {
|
|
|
2626
2628
|
}
|
|
2627
2629
|
return;
|
|
2628
2630
|
}
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
if (
|
|
2634
|
-
|
|
2635
|
-
if (pendingNavigationLoadId > originatingLoadId) {
|
|
2636
|
-
// A new navigation was kicked off after our action started, so that
|
|
2637
|
-
// should take precedence over this redirect navigation. We already
|
|
2638
|
-
// set isRevalidationRequired so all loaders for the new route should
|
|
2639
|
-
// fire unless opted out via shouldRevalidate
|
|
2631
|
+
|
|
2632
|
+
// When using v7_fetcherPersist, we don't want errors bubbling up to the UI
|
|
2633
|
+
// or redirects processed for unmounted fetchers so we just revert them to
|
|
2634
|
+
// idle
|
|
2635
|
+
if (future.v7_fetcherPersist && deletedFetchers.has(key)) {
|
|
2636
|
+
if (isRedirectResult(actionResult) || isErrorResult(actionResult)) {
|
|
2640
2637
|
updateFetcherState(key, getDoneFetcher(undefined));
|
|
2641
2638
|
return;
|
|
2642
|
-
} else {
|
|
2643
|
-
fetchRedirectIds.add(key);
|
|
2644
|
-
updateFetcherState(key, getLoadingFetcher(submission));
|
|
2645
|
-
return startRedirectNavigation(state, actionResult, {
|
|
2646
|
-
fetcherSubmission: submission
|
|
2647
|
-
});
|
|
2648
2639
|
}
|
|
2649
|
-
|
|
2640
|
+
// Let SuccessResult's fall through for revalidation
|
|
2641
|
+
} else {
|
|
2642
|
+
if (isRedirectResult(actionResult)) {
|
|
2643
|
+
fetchControllers.delete(key);
|
|
2644
|
+
if (pendingNavigationLoadId > originatingLoadId) {
|
|
2645
|
+
// A new navigation was kicked off after our action started, so that
|
|
2646
|
+
// should take precedence over this redirect navigation. We already
|
|
2647
|
+
// set isRevalidationRequired so all loaders for the new route should
|
|
2648
|
+
// fire unless opted out via shouldRevalidate
|
|
2649
|
+
updateFetcherState(key, getDoneFetcher(undefined));
|
|
2650
|
+
return;
|
|
2651
|
+
} else {
|
|
2652
|
+
fetchRedirectIds.add(key);
|
|
2653
|
+
updateFetcherState(key, getLoadingFetcher(submission));
|
|
2654
|
+
return startRedirectNavigation(state, actionResult, {
|
|
2655
|
+
fetcherSubmission: submission
|
|
2656
|
+
});
|
|
2657
|
+
}
|
|
2658
|
+
}
|
|
2650
2659
|
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2660
|
+
// Process any non-redirect errors thrown
|
|
2661
|
+
if (isErrorResult(actionResult)) {
|
|
2662
|
+
setFetcherError(key, routeId, actionResult.error);
|
|
2663
|
+
return;
|
|
2664
|
+
}
|
|
2655
2665
|
}
|
|
2656
2666
|
if (isDeferredResult(actionResult)) {
|
|
2657
2667
|
throw getInternalRouterError(400, {
|
|
@@ -2770,7 +2780,7 @@ function createRouter(init) {
|
|
|
2770
2780
|
let fetchRequest = createClientSideRequest(init.history, path, abortController.signal);
|
|
2771
2781
|
fetchControllers.set(key, abortController);
|
|
2772
2782
|
let originatingLoadId = incrementingLoadId;
|
|
2773
|
-
let result = await callLoaderOrAction("loader", fetchRequest, match, matches, manifest, mapRouteProperties, basename);
|
|
2783
|
+
let result = await callLoaderOrAction("loader", fetchRequest, match, matches, manifest, mapRouteProperties, basename, future.v7_relativeSplatPath);
|
|
2774
2784
|
|
|
2775
2785
|
// Deferred isn't supported for fetcher loads, await everything and treat it
|
|
2776
2786
|
// as a normal load. resolveDeferredData will return undefined if this
|
|
@@ -2788,6 +2798,9 @@ function createRouter(init) {
|
|
|
2788
2798
|
if (fetchRequest.signal.aborted) {
|
|
2789
2799
|
return;
|
|
2790
2800
|
}
|
|
2801
|
+
|
|
2802
|
+
// We don't want errors bubbling up or redirects followed for unmounted
|
|
2803
|
+
// fetchers, so short circuit here if it was removed from the UI
|
|
2791
2804
|
if (deletedFetchers.has(key)) {
|
|
2792
2805
|
updateFetcherState(key, getDoneFetcher(undefined));
|
|
2793
2806
|
return;
|
|
@@ -2918,9 +2931,9 @@ function createRouter(init) {
|
|
|
2918
2931
|
// Call all navigation loaders and revalidating fetcher loaders in parallel,
|
|
2919
2932
|
// then slice off the results into separate arrays so we can handle them
|
|
2920
2933
|
// accordingly
|
|
2921
|
-
let results = await Promise.all([...matchesToLoad.map(match => callLoaderOrAction("loader", request, match, matches, manifest, mapRouteProperties, basename)), ...fetchersToLoad.map(f => {
|
|
2934
|
+
let results = await Promise.all([...matchesToLoad.map(match => callLoaderOrAction("loader", request, match, matches, manifest, mapRouteProperties, basename, future.v7_relativeSplatPath)), ...fetchersToLoad.map(f => {
|
|
2922
2935
|
if (f.matches && f.match && f.controller) {
|
|
2923
|
-
return callLoaderOrAction("loader", createClientSideRequest(init.history, f.path, f.controller.signal), f.match, f.matches, manifest, mapRouteProperties, basename);
|
|
2936
|
+
return callLoaderOrAction("loader", createClientSideRequest(init.history, f.path, f.controller.signal), f.match, f.matches, manifest, mapRouteProperties, basename, future.v7_relativeSplatPath);
|
|
2924
2937
|
} else {
|
|
2925
2938
|
let error = {
|
|
2926
2939
|
type: ResultType.error,
|
|
@@ -3239,6 +3252,11 @@ function createRouter(init) {
|
|
|
3239
3252
|
////////////////////////////////////////////////////////////////////////////////
|
|
3240
3253
|
|
|
3241
3254
|
const UNSAFE_DEFERRED_SYMBOL = Symbol("deferred");
|
|
3255
|
+
|
|
3256
|
+
/**
|
|
3257
|
+
* Future flags to toggle new feature behavior
|
|
3258
|
+
*/
|
|
3259
|
+
|
|
3242
3260
|
function createStaticHandler(routes, opts) {
|
|
3243
3261
|
invariant(routes.length > 0, "You must provide a non-empty routes array to createStaticHandler");
|
|
3244
3262
|
let manifest = {};
|
|
@@ -3255,6 +3273,10 @@ function createStaticHandler(routes, opts) {
|
|
|
3255
3273
|
} else {
|
|
3256
3274
|
mapRouteProperties = defaultMapRouteProperties;
|
|
3257
3275
|
}
|
|
3276
|
+
// Config driven behavior flags
|
|
3277
|
+
let future = _extends({
|
|
3278
|
+
v7_relativeSplatPath: false
|
|
3279
|
+
}, opts ? opts.future : null);
|
|
3258
3280
|
let dataRoutes = convertRoutesToDataRoutes(routes, mapRouteProperties, undefined, manifest);
|
|
3259
3281
|
|
|
3260
3282
|
/**
|
|
@@ -3470,7 +3492,7 @@ function createStaticHandler(routes, opts) {
|
|
|
3470
3492
|
error
|
|
3471
3493
|
};
|
|
3472
3494
|
} else {
|
|
3473
|
-
result = await callLoaderOrAction("action", request, actionMatch, matches, manifest, mapRouteProperties, basename, {
|
|
3495
|
+
result = await callLoaderOrAction("action", request, actionMatch, matches, manifest, mapRouteProperties, basename, future.v7_relativeSplatPath, {
|
|
3474
3496
|
isStaticRequest: true,
|
|
3475
3497
|
isRouteRequest,
|
|
3476
3498
|
requestContext
|
|
@@ -3589,7 +3611,7 @@ function createStaticHandler(routes, opts) {
|
|
|
3589
3611
|
activeDeferreds: null
|
|
3590
3612
|
};
|
|
3591
3613
|
}
|
|
3592
|
-
let results = await Promise.all([...matchesToLoad.map(match => callLoaderOrAction("loader", request, match, matches, manifest, mapRouteProperties, basename, {
|
|
3614
|
+
let results = await Promise.all([...matchesToLoad.map(match => callLoaderOrAction("loader", request, match, matches, manifest, mapRouteProperties, basename, future.v7_relativeSplatPath, {
|
|
3593
3615
|
isStaticRequest: true,
|
|
3594
3616
|
isRouteRequest,
|
|
3595
3617
|
requestContext
|
|
@@ -3644,7 +3666,7 @@ function getStaticContextFromError(routes, context, error) {
|
|
|
3644
3666
|
function isSubmissionNavigation(opts) {
|
|
3645
3667
|
return opts != null && ("formData" in opts && opts.formData != null || "body" in opts && opts.body !== undefined);
|
|
3646
3668
|
}
|
|
3647
|
-
function normalizeTo(location, matches, basename, prependBasename, to, fromRouteId, relative) {
|
|
3669
|
+
function normalizeTo(location, matches, basename, prependBasename, to, v7_relativeSplatPath, fromRouteId, relative) {
|
|
3648
3670
|
let contextualMatches;
|
|
3649
3671
|
let activeRouteMatch;
|
|
3650
3672
|
if (fromRouteId) {
|
|
@@ -3664,7 +3686,7 @@ function normalizeTo(location, matches, basename, prependBasename, to, fromRoute
|
|
|
3664
3686
|
}
|
|
3665
3687
|
|
|
3666
3688
|
// Resolve the relative path
|
|
3667
|
-
let path = resolveTo(to ? to : ".", getResolveToMatches(contextualMatches), stripBasename(location.pathname, basename) || location.pathname, relative === "path");
|
|
3689
|
+
let path = resolveTo(to ? to : ".", getResolveToMatches(contextualMatches, v7_relativeSplatPath), stripBasename(location.pathname, basename) || location.pathname, relative === "path");
|
|
3668
3690
|
|
|
3669
3691
|
// When `to` is not specified we inherit search/hash from the current
|
|
3670
3692
|
// location, unlike when to="." and we just inherit the path.
|
|
@@ -3837,18 +3859,24 @@ function getMatchesToLoad(history, state, matches, submission, location, isIniti
|
|
|
3837
3859
|
let boundaryId = pendingError ? Object.keys(pendingError)[0] : undefined;
|
|
3838
3860
|
let boundaryMatches = getLoaderMatchesUntilBoundary(matches, boundaryId);
|
|
3839
3861
|
let navigationMatches = boundaryMatches.filter((match, index) => {
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
}
|
|
3845
|
-
if (match.route.lazy) {
|
|
3862
|
+
let {
|
|
3863
|
+
route
|
|
3864
|
+
} = match;
|
|
3865
|
+
if (route.lazy) {
|
|
3846
3866
|
// We haven't loaded this route yet so we don't know if it's got a loader!
|
|
3847
3867
|
return true;
|
|
3848
3868
|
}
|
|
3849
|
-
if (
|
|
3869
|
+
if (route.loader == null) {
|
|
3850
3870
|
return false;
|
|
3851
3871
|
}
|
|
3872
|
+
if (isInitialLoad) {
|
|
3873
|
+
if (route.loader.hydrate) {
|
|
3874
|
+
return true;
|
|
3875
|
+
}
|
|
3876
|
+
return state.loaderData[route.id] === undefined && (
|
|
3877
|
+
// Don't re-run if the loader ran and threw an error
|
|
3878
|
+
!state.errors || state.errors[route.id] === undefined);
|
|
3879
|
+
}
|
|
3852
3880
|
|
|
3853
3881
|
// Always call the loader on new route instances and pending defer cancellations
|
|
3854
3882
|
if (isNewLoader(state.loaderData, state.matches[index], match) || cancelledDeferredRoutes.some(id => id === match.route.id)) {
|
|
@@ -3950,20 +3978,6 @@ function getMatchesToLoad(history, state, matches, submission, location, isIniti
|
|
|
3950
3978
|
});
|
|
3951
3979
|
return [navigationMatches, revalidatingFetchers];
|
|
3952
3980
|
}
|
|
3953
|
-
|
|
3954
|
-
// Is this route unhydrated (when v7_partialHydration=true) such that we need
|
|
3955
|
-
// to call it's loader on the initial router creation
|
|
3956
|
-
function isUnhydratedRoute(state, route) {
|
|
3957
|
-
if (!route.loader) {
|
|
3958
|
-
return false;
|
|
3959
|
-
}
|
|
3960
|
-
if (route.loader.hydrate) {
|
|
3961
|
-
return true;
|
|
3962
|
-
}
|
|
3963
|
-
return state.loaderData[route.id] === undefined && (!state.errors ||
|
|
3964
|
-
// Loader ran but errored - don't re-run
|
|
3965
|
-
state.errors[route.id] === undefined);
|
|
3966
|
-
}
|
|
3967
3981
|
function isNewLoader(currentLoaderData, currentMatch, match) {
|
|
3968
3982
|
let isNew =
|
|
3969
3983
|
// [a] -> [a, b]
|
|
@@ -4050,7 +4064,7 @@ async function loadLazyRouteModule(route, mapRouteProperties, manifest) {
|
|
|
4050
4064
|
lazy: undefined
|
|
4051
4065
|
}));
|
|
4052
4066
|
}
|
|
4053
|
-
async function callLoaderOrAction(type, request, match, matches, manifest, mapRouteProperties, basename, opts) {
|
|
4067
|
+
async function callLoaderOrAction(type, request, match, matches, manifest, mapRouteProperties, basename, v7_relativeSplatPath, opts) {
|
|
4054
4068
|
if (opts === void 0) {
|
|
4055
4069
|
opts = {};
|
|
4056
4070
|
}
|
|
@@ -4140,7 +4154,7 @@ async function callLoaderOrAction(type, request, match, matches, manifest, mapRo
|
|
|
4140
4154
|
|
|
4141
4155
|
// Support relative routing in internal redirects
|
|
4142
4156
|
if (!ABSOLUTE_URL_REGEX.test(location)) {
|
|
4143
|
-
location = normalizeTo(new URL(request.url), matches.slice(0, matches.indexOf(match) + 1), basename, true, location);
|
|
4157
|
+
location = normalizeTo(new URL(request.url), matches.slice(0, matches.indexOf(match) + 1), basename, true, location, v7_relativeSplatPath);
|
|
4144
4158
|
} else if (!opts.isStaticRequest) {
|
|
4145
4159
|
// Strip off the protocol+origin for same-origin + same-basename absolute
|
|
4146
4160
|
// redirects. If this is a static request, we can let it go back to the
|
|
@@ -4181,13 +4195,20 @@ async function callLoaderOrAction(type, request, match, matches, manifest, mapRo
|
|
|
4181
4195
|
throw queryRouteResponse;
|
|
4182
4196
|
}
|
|
4183
4197
|
let data;
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4198
|
+
try {
|
|
4199
|
+
let contentType = result.headers.get("Content-Type");
|
|
4200
|
+
// Check between word boundaries instead of startsWith() due to the last
|
|
4201
|
+
// paragraph of https://httpwg.org/specs/rfc9110.html#field.content-type
|
|
4202
|
+
if (contentType && /\bapplication\/json\b/.test(contentType)) {
|
|
4203
|
+
data = await result.json();
|
|
4204
|
+
} else {
|
|
4205
|
+
data = await result.text();
|
|
4206
|
+
}
|
|
4207
|
+
} catch (e) {
|
|
4208
|
+
return {
|
|
4209
|
+
type: ResultType.error,
|
|
4210
|
+
error: e
|
|
4211
|
+
};
|
|
4191
4212
|
}
|
|
4192
4213
|
if (resultType === ResultType.error) {
|
|
4193
4214
|
return {
|