@remix-run/router 1.13.1 → 1.14.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 +187 -0
- package/dist/index.d.ts +1 -1
- package/dist/router.cjs.js +117 -39
- package/dist/router.cjs.js.map +1 -1
- package/dist/router.d.ts +16 -0
- package/dist/router.js +109 -39
- package/dist/router.js.map +1 -1
- package/dist/router.umd.js +117 -39
- 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 +5 -2
- package/index.ts +1 -1
- package/package.json +1 -1
- package/router.ts +131 -21
- package/utils.ts +27 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,192 @@
|
|
|
1
1
|
# `@remix-run/router`
|
|
2
2
|
|
|
3
|
+
## 1.14.0-pre.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [REMOVE] Refactor internals for partial hydration ([#11094](https://github.com/remix-run/react-router/pull/11094))
|
|
8
|
+
|
|
9
|
+
## 1.14.0-pre.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- 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))
|
|
14
|
+
|
|
15
|
+
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`.
|
|
16
|
+
|
|
17
|
+
```jsx
|
|
18
|
+
let router = createBrowserRouter(
|
|
19
|
+
[
|
|
20
|
+
{
|
|
21
|
+
id: "root",
|
|
22
|
+
path: "/",
|
|
23
|
+
loader: rootLoader,
|
|
24
|
+
Component: RootComponent,
|
|
25
|
+
Fallback: RootFallback,
|
|
26
|
+
children: [
|
|
27
|
+
{
|
|
28
|
+
id: "index",
|
|
29
|
+
index: true,
|
|
30
|
+
loader: indexLoader,
|
|
31
|
+
Component: IndexComponent,
|
|
32
|
+
HydrateFallback: IndexFallback,
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
{
|
|
38
|
+
future: {
|
|
39
|
+
v7_partialHydration: true,
|
|
40
|
+
},
|
|
41
|
+
hydrationData: {
|
|
42
|
+
loaderData: {
|
|
43
|
+
root: { message: "Hydrated from Root!" },
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
);
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
If the above example did not have an `IndexFallback`, then `RouterProvider` would instead render the `RootFallback` while it executed the `indexLoader`.
|
|
51
|
+
|
|
52
|
+
**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.
|
|
53
|
+
|
|
54
|
+
- 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))
|
|
55
|
+
|
|
56
|
+
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))
|
|
57
|
+
|
|
58
|
+
**The Bug**
|
|
59
|
+
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.
|
|
60
|
+
|
|
61
|
+
**The Background**
|
|
62
|
+
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:
|
|
63
|
+
|
|
64
|
+
```jsx
|
|
65
|
+
<BrowserRouter>
|
|
66
|
+
<Routes>
|
|
67
|
+
<Route path="/" element={<Home />} />
|
|
68
|
+
<Route path="dashboard/*" element={<Dashboard />} />
|
|
69
|
+
</Routes>
|
|
70
|
+
</BrowserRouter>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Any paths like `/dashboard`, `/dashboard/team`, `/dashboard/projects` will match the `Dashboard` route. The dashboard component itself can then render nested `<Routes>`:
|
|
74
|
+
|
|
75
|
+
```jsx
|
|
76
|
+
function Dashboard() {
|
|
77
|
+
return (
|
|
78
|
+
<div>
|
|
79
|
+
<h2>Dashboard</h2>
|
|
80
|
+
<nav>
|
|
81
|
+
<Link to="/">Dashboard Home</Link>
|
|
82
|
+
<Link to="team">Team</Link>
|
|
83
|
+
<Link to="projects">Projects</Link>
|
|
84
|
+
</nav>
|
|
85
|
+
|
|
86
|
+
<Routes>
|
|
87
|
+
<Route path="/" element={<DashboardHome />} />
|
|
88
|
+
<Route path="team" element={<DashboardTeam />} />
|
|
89
|
+
<Route path="projects" element={<DashboardProjects />} />
|
|
90
|
+
</Routes>
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
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.
|
|
97
|
+
|
|
98
|
+
**The Problem**
|
|
99
|
+
|
|
100
|
+
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 `"."`:
|
|
101
|
+
|
|
102
|
+
```jsx
|
|
103
|
+
// If we are on URL /dashboard/team, and we want to link to /dashboard/team:
|
|
104
|
+
function DashboardTeam() {
|
|
105
|
+
// ❌ This is broken and results in <a href="/dashboard">
|
|
106
|
+
return <Link to=".">A broken link to the Current URL</Link>;
|
|
107
|
+
|
|
108
|
+
// ✅ This is fixed but super unintuitive since we're already at /dashboard/team!
|
|
109
|
+
return <Link to="./team">A broken link to the Current URL</Link>;
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
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.
|
|
114
|
+
|
|
115
|
+
Even worse, consider a nested splat route configuration:
|
|
116
|
+
|
|
117
|
+
```jsx
|
|
118
|
+
<BrowserRouter>
|
|
119
|
+
<Routes>
|
|
120
|
+
<Route path="dashboard">
|
|
121
|
+
<Route path="*" element={<Dashboard />} />
|
|
122
|
+
</Route>
|
|
123
|
+
</Routes>
|
|
124
|
+
</BrowserRouter>
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Now, a `<Link to=".">` and a `<Link to="..">` inside the `Dashboard` component go to the same place! That is definitely not correct!
|
|
128
|
+
|
|
129
|
+
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:
|
|
130
|
+
|
|
131
|
+
```jsx
|
|
132
|
+
let router = createBrowserRouter({
|
|
133
|
+
path: "/dashboard",
|
|
134
|
+
children: [
|
|
135
|
+
{
|
|
136
|
+
path: "*",
|
|
137
|
+
action: dashboardAction,
|
|
138
|
+
Component() {
|
|
139
|
+
// ❌ This form is broken! It throws a 405 error when it submits because
|
|
140
|
+
// it tries to submit to /dashboard (without the splat value) and the parent
|
|
141
|
+
// `/dashboard` route doesn't have an action
|
|
142
|
+
return <Form method="post">...</Form>;
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
});
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
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.
|
|
150
|
+
|
|
151
|
+
**The Solution**
|
|
152
|
+
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:
|
|
153
|
+
|
|
154
|
+
```jsx
|
|
155
|
+
<BrowserRouter>
|
|
156
|
+
<Routes>
|
|
157
|
+
<Route path="dashboard">
|
|
158
|
+
<Route path="*" element={<Dashboard />} />
|
|
159
|
+
</Route>
|
|
160
|
+
</Routes>
|
|
161
|
+
</BrowserRouter>
|
|
162
|
+
|
|
163
|
+
function Dashboard() {
|
|
164
|
+
return (
|
|
165
|
+
<div>
|
|
166
|
+
<h2>Dashboard</h2>
|
|
167
|
+
<nav>
|
|
168
|
+
<Link to="..">Dashboard Home</Link>
|
|
169
|
+
<Link to="../team">Team</Link>
|
|
170
|
+
<Link to="../projects">Projects</Link>
|
|
171
|
+
</nav>
|
|
172
|
+
|
|
173
|
+
<Routes>
|
|
174
|
+
<Route path="/" element={<DashboardHome />} />
|
|
175
|
+
<Route path="team" element={<DashboardTeam />} />
|
|
176
|
+
<Route path="projects" element={<DashboardProjects />} />
|
|
177
|
+
</Router>
|
|
178
|
+
</div>
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
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".
|
|
184
|
+
|
|
185
|
+
### Patch Changes
|
|
186
|
+
|
|
187
|
+
- Catch and bubble errors thrown when trying to unwrap responses from `loader`/`action` functions ([#11061](https://github.com/remix-run/react-router/pull/11061))
|
|
188
|
+
- Fix `relative="path"` issue when rendering `Link`/`NavLink` outside of matched routes ([#11062](https://github.com/remix-run/react-router/pull/11062))
|
|
189
|
+
|
|
3
190
|
## 1.13.1
|
|
4
191
|
|
|
5
192
|
### Patch Changes
|
package/dist/index.d.ts
CHANGED
|
@@ -5,5 +5,5 @@ export { Action, createBrowserHistory, createHashHistory, createMemoryHistory, c
|
|
|
5
5
|
export * from "./router";
|
|
6
6
|
/** @internal */
|
|
7
7
|
export type { RouteManifest as UNSAFE_RouteManifest } from "./utils";
|
|
8
|
-
export { DeferredData as UNSAFE_DeferredData, ErrorResponseImpl as UNSAFE_ErrorResponseImpl, convertRoutesToDataRoutes as UNSAFE_convertRoutesToDataRoutes, convertRouteMatchToUiMatch as UNSAFE_convertRouteMatchToUiMatch,
|
|
8
|
+
export { DeferredData as UNSAFE_DeferredData, ErrorResponseImpl as UNSAFE_ErrorResponseImpl, convertRoutesToDataRoutes as UNSAFE_convertRoutesToDataRoutes, convertRouteMatchToUiMatch as UNSAFE_convertRouteMatchToUiMatch, getResolveToMatches as UNSAFE_getResolveToMatches, } from "./utils";
|
|
9
9
|
export { invariant as UNSAFE_invariant, warning as UNSAFE_warning, } from "./history";
|
package/dist/router.cjs.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @remix-run/router v1.
|
|
2
|
+
* @remix-run/router v1.14.0-pre.1
|
|
3
3
|
*
|
|
4
4
|
* Copyright (c) Remix Software Inc.
|
|
5
5
|
*
|
|
@@ -1192,6 +1192,20 @@ function getPathContributingMatches(matches) {
|
|
|
1192
1192
|
return matches.filter((match, index) => index === 0 || match.route.path && match.route.path.length > 0);
|
|
1193
1193
|
}
|
|
1194
1194
|
|
|
1195
|
+
// Return the array of pathnames for the current route matches - used to
|
|
1196
|
+
// generate the routePathnames input for resolveTo()
|
|
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);
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1195
1209
|
/**
|
|
1196
1210
|
* @private
|
|
1197
1211
|
*/
|
|
@@ -1224,7 +1238,7 @@ function resolveTo(toArg, routePathnames, locationPathname, isPathRelative) {
|
|
|
1224
1238
|
if (toPathname == null) {
|
|
1225
1239
|
from = locationPathname;
|
|
1226
1240
|
} else if (isPathRelative) {
|
|
1227
|
-
let fromSegments = routePathnames[routePathnames.length - 1].replace(/^\//, "").split("/");
|
|
1241
|
+
let fromSegments = routePathnames.length === 0 ? [] : routePathnames[routePathnames.length - 1].replace(/^\//, "").split("/");
|
|
1228
1242
|
if (toPathname.startsWith("..")) {
|
|
1229
1243
|
let toSegments = toPathname.split("/");
|
|
1230
1244
|
|
|
@@ -1679,7 +1693,9 @@ function createRouter(init) {
|
|
|
1679
1693
|
let future = _extends({
|
|
1680
1694
|
v7_fetcherPersist: false,
|
|
1681
1695
|
v7_normalizeFormMethod: false,
|
|
1682
|
-
|
|
1696
|
+
v7_partialHydration: false,
|
|
1697
|
+
v7_prependBasename: false,
|
|
1698
|
+
v7_relativeSplatPath: false
|
|
1683
1699
|
}, init.future);
|
|
1684
1700
|
// Cleanup function for history
|
|
1685
1701
|
let unlistenHistory = null;
|
|
@@ -1715,12 +1731,28 @@ function createRouter(init) {
|
|
|
1715
1731
|
[route.id]: error
|
|
1716
1732
|
};
|
|
1717
1733
|
}
|
|
1718
|
-
let initialized
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1734
|
+
let initialized;
|
|
1735
|
+
let hasLazyRoutes = initialMatches.some(m => m.route.lazy);
|
|
1736
|
+
let hasLoaders = initialMatches.some(m => m.route.loader);
|
|
1737
|
+
if (hasLazyRoutes) {
|
|
1738
|
+
// All initialMatches need to be loaded before we're ready. If we have lazy
|
|
1739
|
+
// functions around still then we'll need to run them in initialize()
|
|
1740
|
+
initialized = false;
|
|
1741
|
+
} else if (!hasLoaders) {
|
|
1742
|
+
// If we've got no loaders to run, then we're good to go
|
|
1743
|
+
initialized = true;
|
|
1744
|
+
} else if (future.v7_partialHydration) {
|
|
1745
|
+
// If partial hydration is enabled, we're initialized so long as we were
|
|
1746
|
+
// provided with hydrationData for every route with a loader, and no loaders
|
|
1747
|
+
// were marked for explicit hydration
|
|
1748
|
+
let loaderData = init.hydrationData ? init.hydrationData.loaderData : null;
|
|
1749
|
+
let errors = init.hydrationData ? init.hydrationData.errors : null;
|
|
1750
|
+
initialized = initialMatches.every(m => m.route.loader && m.route.loader.hydrate !== true && (loaderData && loaderData[m.route.id] !== undefined || errors && errors[m.route.id] !== undefined));
|
|
1751
|
+
} else {
|
|
1752
|
+
// Without partial hydration - we're initialized if we were provided any
|
|
1753
|
+
// hydrationData - which is expected to be complete
|
|
1754
|
+
initialized = init.hydrationData != null;
|
|
1755
|
+
}
|
|
1724
1756
|
let router;
|
|
1725
1757
|
let state = {
|
|
1726
1758
|
historyAction: init.history.action,
|
|
@@ -1888,7 +1920,9 @@ function createRouter(init) {
|
|
|
1888
1920
|
// resolved prior to router creation since we can't go into a fallbackElement
|
|
1889
1921
|
// UI for SSR'd apps
|
|
1890
1922
|
if (!state.initialized) {
|
|
1891
|
-
startNavigation(Action.Pop, state.location
|
|
1923
|
+
startNavigation(Action.Pop, state.location, {
|
|
1924
|
+
initialHydration: true
|
|
1925
|
+
});
|
|
1892
1926
|
}
|
|
1893
1927
|
return router;
|
|
1894
1928
|
}
|
|
@@ -2077,7 +2111,7 @@ function createRouter(init) {
|
|
|
2077
2111
|
init.history.go(to);
|
|
2078
2112
|
return;
|
|
2079
2113
|
}
|
|
2080
|
-
let normalizedPath = normalizeTo(state.location, state.matches, basename, future.v7_prependBasename, to, opts == null ? void 0 : opts.fromRouteId, opts == null ? void 0 : opts.relative);
|
|
2114
|
+
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);
|
|
2081
2115
|
let {
|
|
2082
2116
|
path,
|
|
2083
2117
|
submission,
|
|
@@ -2278,7 +2312,7 @@ function createRouter(init) {
|
|
|
2278
2312
|
shortCircuited,
|
|
2279
2313
|
loaderData,
|
|
2280
2314
|
errors
|
|
2281
|
-
} = await handleLoaders(request, location, matches, loadingNavigation, opts && opts.submission, opts && opts.fetcherSubmission, opts && opts.replace, flushSync, pendingActionData, pendingError);
|
|
2315
|
+
} = await handleLoaders(request, location, matches, loadingNavigation, opts && opts.submission, opts && opts.fetcherSubmission, opts && opts.replace, opts && opts.initialHydration === true, flushSync, pendingActionData, pendingError);
|
|
2282
2316
|
if (shortCircuited) {
|
|
2283
2317
|
return;
|
|
2284
2318
|
}
|
|
@@ -2326,7 +2360,7 @@ function createRouter(init) {
|
|
|
2326
2360
|
})
|
|
2327
2361
|
};
|
|
2328
2362
|
} else {
|
|
2329
|
-
result = await callLoaderOrAction("action", request, actionMatch, matches, manifest, mapRouteProperties, basename);
|
|
2363
|
+
result = await callLoaderOrAction("action", request, actionMatch, matches, manifest, mapRouteProperties, basename, future.v7_relativeSplatPath);
|
|
2330
2364
|
if (request.signal.aborted) {
|
|
2331
2365
|
return {
|
|
2332
2366
|
shortCircuited: true
|
|
@@ -2385,7 +2419,7 @@ function createRouter(init) {
|
|
|
2385
2419
|
|
|
2386
2420
|
// Call all applicable loaders for the given matches, handling redirects,
|
|
2387
2421
|
// errors, etc.
|
|
2388
|
-
async function handleLoaders(request, location, matches, overrideNavigation, submission, fetcherSubmission, replace, flushSync, pendingActionData, pendingError) {
|
|
2422
|
+
async function handleLoaders(request, location, matches, overrideNavigation, submission, fetcherSubmission, replace, initialHydration, flushSync, pendingActionData, pendingError) {
|
|
2389
2423
|
// Figure out the right navigation we want to use for data loading
|
|
2390
2424
|
let loadingNavigation = overrideNavigation || getLoadingNavigation(location, submission);
|
|
2391
2425
|
|
|
@@ -2393,7 +2427,7 @@ function createRouter(init) {
|
|
|
2393
2427
|
// we have it on the loading navigation so use that if available
|
|
2394
2428
|
let activeSubmission = submission || fetcherSubmission || getSubmissionFromNavigation(loadingNavigation);
|
|
2395
2429
|
let routesToUse = inFlightDataRoutes || dataRoutes;
|
|
2396
|
-
let [matchesToLoad, revalidatingFetchers] = getMatchesToLoad(init.history, state, matches, activeSubmission, location, isRevalidationRequired, cancelledDeferredRoutes, cancelledFetcherLoads, deletedFetchers, fetchLoadMatches, fetchRedirectIds, routesToUse, basename, pendingActionData, pendingError);
|
|
2430
|
+
let [matchesToLoad, revalidatingFetchers] = getMatchesToLoad(init.history, state, matches, activeSubmission, location, future.v7_partialHydration && initialHydration === true, isRevalidationRequired, cancelledDeferredRoutes, cancelledFetcherLoads, deletedFetchers, fetchLoadMatches, fetchRedirectIds, routesToUse, basename, pendingActionData, pendingError);
|
|
2397
2431
|
|
|
2398
2432
|
// Cancel pending deferreds for no-longer-matched routes or routes we're
|
|
2399
2433
|
// about to reload. Note that if this is an action reload we would have
|
|
@@ -2425,7 +2459,9 @@ function createRouter(init) {
|
|
|
2425
2459
|
// state. If not, we need to switch to our loading state and load data,
|
|
2426
2460
|
// preserving any new action data or existing action data (in the case of
|
|
2427
2461
|
// a revalidation interrupting an actionReload)
|
|
2428
|
-
|
|
2462
|
+
// If we have partialHydration enabled, then don't update the state for the
|
|
2463
|
+
// initial data load since iot's not a "navigation"
|
|
2464
|
+
if (!isUninterruptedRevalidation && (!future.v7_partialHydration || !initialHydration)) {
|
|
2429
2465
|
revalidatingFetchers.forEach(rf => {
|
|
2430
2466
|
let fetcher = state.fetchers.get(rf.key);
|
|
2431
2467
|
let revalidatingFetcher = getLoadingFetcher(undefined, fetcher ? fetcher.data : undefined);
|
|
@@ -2534,7 +2570,7 @@ function createRouter(init) {
|
|
|
2534
2570
|
if (fetchControllers.has(key)) abortFetcher(key);
|
|
2535
2571
|
let flushSync = (opts && opts.unstable_flushSync) === true;
|
|
2536
2572
|
let routesToUse = inFlightDataRoutes || dataRoutes;
|
|
2537
|
-
let normalizedPath = normalizeTo(state.location, state.matches, basename, future.v7_prependBasename, href, routeId, opts == null ? void 0 : opts.relative);
|
|
2573
|
+
let normalizedPath = normalizeTo(state.location, state.matches, basename, future.v7_prependBasename, href, future.v7_relativeSplatPath, routeId, opts == null ? void 0 : opts.relative);
|
|
2538
2574
|
let matches = matchRoutes(routesToUse, normalizedPath, basename);
|
|
2539
2575
|
if (!matches) {
|
|
2540
2576
|
setFetcherError(key, routeId, getInternalRouterError(404, {
|
|
@@ -2599,7 +2635,7 @@ function createRouter(init) {
|
|
|
2599
2635
|
let fetchRequest = createClientSideRequest(init.history, path, abortController.signal, submission);
|
|
2600
2636
|
fetchControllers.set(key, abortController);
|
|
2601
2637
|
let originatingLoadId = incrementingLoadId;
|
|
2602
|
-
let actionResult = await callLoaderOrAction("action", fetchRequest, match, requestMatches, manifest, mapRouteProperties, basename);
|
|
2638
|
+
let actionResult = await callLoaderOrAction("action", fetchRequest, match, requestMatches, manifest, mapRouteProperties, basename, future.v7_relativeSplatPath);
|
|
2603
2639
|
if (fetchRequest.signal.aborted) {
|
|
2604
2640
|
// We can delete this so long as we weren't aborted by our own fetcher
|
|
2605
2641
|
// re-submit which would have put _new_ controller is in fetchControllers
|
|
@@ -2652,7 +2688,7 @@ function createRouter(init) {
|
|
|
2652
2688
|
fetchReloadIds.set(key, loadId);
|
|
2653
2689
|
let loadFetcher = getLoadingFetcher(submission, actionResult.data);
|
|
2654
2690
|
state.fetchers.set(key, loadFetcher);
|
|
2655
|
-
let [matchesToLoad, revalidatingFetchers] = getMatchesToLoad(init.history, state, matches, submission, nextLocation, isRevalidationRequired, cancelledDeferredRoutes, cancelledFetcherLoads, deletedFetchers, fetchLoadMatches, fetchRedirectIds, routesToUse, basename, {
|
|
2691
|
+
let [matchesToLoad, revalidatingFetchers] = getMatchesToLoad(init.history, state, matches, submission, nextLocation, false, isRevalidationRequired, cancelledDeferredRoutes, cancelledFetcherLoads, deletedFetchers, fetchLoadMatches, fetchRedirectIds, routesToUse, basename, {
|
|
2656
2692
|
[match.route.id]: actionResult.data
|
|
2657
2693
|
}, undefined // No need to send through errors since we short circuit above
|
|
2658
2694
|
);
|
|
@@ -2752,7 +2788,7 @@ function createRouter(init) {
|
|
|
2752
2788
|
let fetchRequest = createClientSideRequest(init.history, path, abortController.signal);
|
|
2753
2789
|
fetchControllers.set(key, abortController);
|
|
2754
2790
|
let originatingLoadId = incrementingLoadId;
|
|
2755
|
-
let result = await callLoaderOrAction("loader", fetchRequest, match, matches, manifest, mapRouteProperties, basename);
|
|
2791
|
+
let result = await callLoaderOrAction("loader", fetchRequest, match, matches, manifest, mapRouteProperties, basename, future.v7_relativeSplatPath);
|
|
2756
2792
|
|
|
2757
2793
|
// Deferred isn't supported for fetcher loads, await everything and treat it
|
|
2758
2794
|
// as a normal load. resolveDeferredData will return undefined if this
|
|
@@ -2900,9 +2936,9 @@ function createRouter(init) {
|
|
|
2900
2936
|
// Call all navigation loaders and revalidating fetcher loaders in parallel,
|
|
2901
2937
|
// then slice off the results into separate arrays so we can handle them
|
|
2902
2938
|
// accordingly
|
|
2903
|
-
let results = await Promise.all([...matchesToLoad.map(match => callLoaderOrAction("loader", request, match, matches, manifest, mapRouteProperties, basename)), ...fetchersToLoad.map(f => {
|
|
2939
|
+
let results = await Promise.all([...matchesToLoad.map(match => callLoaderOrAction("loader", request, match, matches, manifest, mapRouteProperties, basename, future.v7_relativeSplatPath)), ...fetchersToLoad.map(f => {
|
|
2904
2940
|
if (f.matches && f.match && f.controller) {
|
|
2905
|
-
return callLoaderOrAction("loader", createClientSideRequest(init.history, f.path, f.controller.signal), f.match, f.matches, manifest, mapRouteProperties, basename);
|
|
2941
|
+
return callLoaderOrAction("loader", createClientSideRequest(init.history, f.path, f.controller.signal), f.match, f.matches, manifest, mapRouteProperties, basename, future.v7_relativeSplatPath);
|
|
2906
2942
|
} else {
|
|
2907
2943
|
let error = {
|
|
2908
2944
|
type: ResultType.error,
|
|
@@ -3179,6 +3215,9 @@ function createRouter(init) {
|
|
|
3179
3215
|
get basename() {
|
|
3180
3216
|
return basename;
|
|
3181
3217
|
},
|
|
3218
|
+
get future() {
|
|
3219
|
+
return future;
|
|
3220
|
+
},
|
|
3182
3221
|
get state() {
|
|
3183
3222
|
return state;
|
|
3184
3223
|
},
|
|
@@ -3218,6 +3257,11 @@ function createRouter(init) {
|
|
|
3218
3257
|
////////////////////////////////////////////////////////////////////////////////
|
|
3219
3258
|
|
|
3220
3259
|
const UNSAFE_DEFERRED_SYMBOL = Symbol("deferred");
|
|
3260
|
+
|
|
3261
|
+
/**
|
|
3262
|
+
* Future flags to toggle new feature behavior
|
|
3263
|
+
*/
|
|
3264
|
+
|
|
3221
3265
|
function createStaticHandler(routes, opts) {
|
|
3222
3266
|
invariant(routes.length > 0, "You must provide a non-empty routes array to createStaticHandler");
|
|
3223
3267
|
let manifest = {};
|
|
@@ -3234,6 +3278,10 @@ function createStaticHandler(routes, opts) {
|
|
|
3234
3278
|
} else {
|
|
3235
3279
|
mapRouteProperties = defaultMapRouteProperties;
|
|
3236
3280
|
}
|
|
3281
|
+
// Config driven behavior flags
|
|
3282
|
+
let future = _extends({
|
|
3283
|
+
v7_relativeSplatPath: false
|
|
3284
|
+
}, opts ? opts.future : null);
|
|
3237
3285
|
let dataRoutes = convertRoutesToDataRoutes(routes, mapRouteProperties, undefined, manifest);
|
|
3238
3286
|
|
|
3239
3287
|
/**
|
|
@@ -3449,7 +3497,7 @@ function createStaticHandler(routes, opts) {
|
|
|
3449
3497
|
error
|
|
3450
3498
|
};
|
|
3451
3499
|
} else {
|
|
3452
|
-
result = await callLoaderOrAction("action", request, actionMatch, matches, manifest, mapRouteProperties, basename, {
|
|
3500
|
+
result = await callLoaderOrAction("action", request, actionMatch, matches, manifest, mapRouteProperties, basename, future.v7_relativeSplatPath, {
|
|
3453
3501
|
isStaticRequest: true,
|
|
3454
3502
|
isRouteRequest,
|
|
3455
3503
|
requestContext
|
|
@@ -3568,7 +3616,7 @@ function createStaticHandler(routes, opts) {
|
|
|
3568
3616
|
activeDeferreds: null
|
|
3569
3617
|
};
|
|
3570
3618
|
}
|
|
3571
|
-
let results = await Promise.all([...matchesToLoad.map(match => callLoaderOrAction("loader", request, match, matches, manifest, mapRouteProperties, basename, {
|
|
3619
|
+
let results = await Promise.all([...matchesToLoad.map(match => callLoaderOrAction("loader", request, match, matches, manifest, mapRouteProperties, basename, future.v7_relativeSplatPath, {
|
|
3572
3620
|
isStaticRequest: true,
|
|
3573
3621
|
isRouteRequest,
|
|
3574
3622
|
requestContext
|
|
@@ -3623,7 +3671,7 @@ function getStaticContextFromError(routes, context, error) {
|
|
|
3623
3671
|
function isSubmissionNavigation(opts) {
|
|
3624
3672
|
return opts != null && ("formData" in opts && opts.formData != null || "body" in opts && opts.body !== undefined);
|
|
3625
3673
|
}
|
|
3626
|
-
function normalizeTo(location, matches, basename, prependBasename, to, fromRouteId, relative) {
|
|
3674
|
+
function normalizeTo(location, matches, basename, prependBasename, to, v7_relativeSplatPath, fromRouteId, relative) {
|
|
3627
3675
|
let contextualMatches;
|
|
3628
3676
|
let activeRouteMatch;
|
|
3629
3677
|
if (fromRouteId) {
|
|
@@ -3643,7 +3691,7 @@ function normalizeTo(location, matches, basename, prependBasename, to, fromRoute
|
|
|
3643
3691
|
}
|
|
3644
3692
|
|
|
3645
3693
|
// Resolve the relative path
|
|
3646
|
-
let path = resolveTo(to ? to : ".",
|
|
3694
|
+
let path = resolveTo(to ? to : ".", getResolveToMatches(contextualMatches, v7_relativeSplatPath), stripBasename(location.pathname, basename) || location.pathname, relative === "path");
|
|
3647
3695
|
|
|
3648
3696
|
// When `to` is not specified we inherit search/hash from the current
|
|
3649
3697
|
// location, unlike when to="." and we just inherit the path.
|
|
@@ -3807,7 +3855,7 @@ function getLoaderMatchesUntilBoundary(matches, boundaryId) {
|
|
|
3807
3855
|
}
|
|
3808
3856
|
return boundaryMatches;
|
|
3809
3857
|
}
|
|
3810
|
-
function getMatchesToLoad(history, state, matches, submission, location, isRevalidationRequired, cancelledDeferredRoutes, cancelledFetcherLoads, deletedFetchers, fetchLoadMatches, fetchRedirectIds, routesToUse, basename, pendingActionData, pendingError) {
|
|
3858
|
+
function getMatchesToLoad(history, state, matches, submission, location, isInitialLoad, isRevalidationRequired, cancelledDeferredRoutes, cancelledFetcherLoads, deletedFetchers, fetchLoadMatches, fetchRedirectIds, routesToUse, basename, pendingActionData, pendingError) {
|
|
3811
3859
|
let actionResult = pendingError ? Object.values(pendingError)[0] : pendingActionData ? Object.values(pendingActionData)[0] : undefined;
|
|
3812
3860
|
let currentUrl = history.createURL(state.location);
|
|
3813
3861
|
let nextUrl = history.createURL(location);
|
|
@@ -3816,6 +3864,11 @@ function getMatchesToLoad(history, state, matches, submission, location, isReval
|
|
|
3816
3864
|
let boundaryId = pendingError ? Object.keys(pendingError)[0] : undefined;
|
|
3817
3865
|
let boundaryMatches = getLoaderMatchesUntilBoundary(matches, boundaryId);
|
|
3818
3866
|
let navigationMatches = boundaryMatches.filter((match, index) => {
|
|
3867
|
+
if (isInitialLoad) {
|
|
3868
|
+
// On initial hydration we don't do any shouldRevalidate stuff - we just
|
|
3869
|
+
// call the unhydrated loaders
|
|
3870
|
+
return isUnhydratedRoute(state, match.route);
|
|
3871
|
+
}
|
|
3819
3872
|
if (match.route.lazy) {
|
|
3820
3873
|
// We haven't loaded this route yet so we don't know if it's got a loader!
|
|
3821
3874
|
return true;
|
|
@@ -3855,8 +3908,12 @@ function getMatchesToLoad(history, state, matches, submission, location, isReval
|
|
|
3855
3908
|
// Pick fetcher.loads that need to be revalidated
|
|
3856
3909
|
let revalidatingFetchers = [];
|
|
3857
3910
|
fetchLoadMatches.forEach((f, key) => {
|
|
3858
|
-
// Don't revalidate
|
|
3859
|
-
|
|
3911
|
+
// Don't revalidate:
|
|
3912
|
+
// - on initial load (shouldn't be any fetchers then anyway)
|
|
3913
|
+
// - if fetcher won't be present in the subsequent render
|
|
3914
|
+
// - no longer matches the URL (v7_fetcherPersist=false)
|
|
3915
|
+
// - was unmounted but persisted due to v7_fetcherPersist=true
|
|
3916
|
+
if (isInitialLoad || !matches.some(m => m.route.id === f.routeId) || deletedFetchers.has(key)) {
|
|
3860
3917
|
return;
|
|
3861
3918
|
}
|
|
3862
3919
|
let fetcherMatches = matchRoutes(routesToUse, f.path, basename);
|
|
@@ -3920,6 +3977,20 @@ function getMatchesToLoad(history, state, matches, submission, location, isReval
|
|
|
3920
3977
|
});
|
|
3921
3978
|
return [navigationMatches, revalidatingFetchers];
|
|
3922
3979
|
}
|
|
3980
|
+
|
|
3981
|
+
// Is this route unhydrated (when v7_partialHydration=true) such that we need
|
|
3982
|
+
// to call it's loader on the initial router creation
|
|
3983
|
+
function isUnhydratedRoute(state, route) {
|
|
3984
|
+
if (!route.loader) {
|
|
3985
|
+
return false;
|
|
3986
|
+
}
|
|
3987
|
+
if (route.loader.hydrate) {
|
|
3988
|
+
return true;
|
|
3989
|
+
}
|
|
3990
|
+
return state.loaderData[route.id] === undefined && (!state.errors ||
|
|
3991
|
+
// Loader ran but errored - don't re-run
|
|
3992
|
+
state.errors[route.id] === undefined);
|
|
3993
|
+
}
|
|
3923
3994
|
function isNewLoader(currentLoaderData, currentMatch, match) {
|
|
3924
3995
|
let isNew =
|
|
3925
3996
|
// [a] -> [a, b]
|
|
@@ -4006,7 +4077,7 @@ async function loadLazyRouteModule(route, mapRouteProperties, manifest) {
|
|
|
4006
4077
|
lazy: undefined
|
|
4007
4078
|
}));
|
|
4008
4079
|
}
|
|
4009
|
-
async function callLoaderOrAction(type, request, match, matches, manifest, mapRouteProperties, basename, opts) {
|
|
4080
|
+
async function callLoaderOrAction(type, request, match, matches, manifest, mapRouteProperties, basename, v7_relativeSplatPath, opts) {
|
|
4010
4081
|
if (opts === void 0) {
|
|
4011
4082
|
opts = {};
|
|
4012
4083
|
}
|
|
@@ -4096,7 +4167,7 @@ async function callLoaderOrAction(type, request, match, matches, manifest, mapRo
|
|
|
4096
4167
|
|
|
4097
4168
|
// Support relative routing in internal redirects
|
|
4098
4169
|
if (!ABSOLUTE_URL_REGEX.test(location)) {
|
|
4099
|
-
location = normalizeTo(new URL(request.url), matches.slice(0, matches.indexOf(match) + 1), basename, true, location);
|
|
4170
|
+
location = normalizeTo(new URL(request.url), matches.slice(0, matches.indexOf(match) + 1), basename, true, location, v7_relativeSplatPath);
|
|
4100
4171
|
} else if (!opts.isStaticRequest) {
|
|
4101
4172
|
// Strip off the protocol+origin for same-origin + same-basename absolute
|
|
4102
4173
|
// redirects. If this is a static request, we can let it go back to the
|
|
@@ -4137,13 +4208,20 @@ async function callLoaderOrAction(type, request, match, matches, manifest, mapRo
|
|
|
4137
4208
|
throw queryRouteResponse;
|
|
4138
4209
|
}
|
|
4139
4210
|
let data;
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4211
|
+
try {
|
|
4212
|
+
let contentType = result.headers.get("Content-Type");
|
|
4213
|
+
// Check between word boundaries instead of startsWith() due to the last
|
|
4214
|
+
// paragraph of https://httpwg.org/specs/rfc9110.html#field.content-type
|
|
4215
|
+
if (contentType && /\bapplication\/json\b/.test(contentType)) {
|
|
4216
|
+
data = await result.json();
|
|
4217
|
+
} else {
|
|
4218
|
+
data = await result.text();
|
|
4219
|
+
}
|
|
4220
|
+
} catch (e) {
|
|
4221
|
+
return {
|
|
4222
|
+
type: ResultType.error,
|
|
4223
|
+
error: e
|
|
4224
|
+
};
|
|
4147
4225
|
}
|
|
4148
4226
|
if (resultType === ResultType.error) {
|
|
4149
4227
|
return {
|
|
@@ -4742,7 +4820,7 @@ exports.UNSAFE_DeferredData = DeferredData;
|
|
|
4742
4820
|
exports.UNSAFE_ErrorResponseImpl = ErrorResponseImpl;
|
|
4743
4821
|
exports.UNSAFE_convertRouteMatchToUiMatch = convertRouteMatchToUiMatch;
|
|
4744
4822
|
exports.UNSAFE_convertRoutesToDataRoutes = convertRoutesToDataRoutes;
|
|
4745
|
-
exports.
|
|
4823
|
+
exports.UNSAFE_getResolveToMatches = getResolveToMatches;
|
|
4746
4824
|
exports.UNSAFE_invariant = invariant;
|
|
4747
4825
|
exports.UNSAFE_warning = warning;
|
|
4748
4826
|
exports.createBrowserHistory = createBrowserHistory;
|