Skip to content

Commit 283fa44

Browse files
authored
fix _root.data fetch when basename is set (#12898)
1 parent db1b255 commit 283fa44

File tree

7 files changed

+95
-25
lines changed

7 files changed

+95
-25
lines changed

.changeset/short-comics-fly.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"react-router": patch
3+
---
4+
5+
Fix single fetch `_root.data` requests when a `basename` is used

contributors.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
- akamfoad
1717
- alany411
1818
- alberto
19+
- Aleuck
1920
- alexandernanberg
2021
- alexanderson1993
2122
- alexlbr

integration/single-fetch-test.ts

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,9 @@ const files = {
4242
<Links />
4343
</head>
4444
<body>
45-
<Link to="/">Home</Link><br/>
46-
<Link to="/data">Data</Link><br/>
47-
<Link to="/a/b/c">/a/b/c</Link><br/>
45+
<Link to="/">Go to Home</Link><br/>
46+
<Link to="/data">Go to Data</Link><br/>
47+
<Link to="/a/b/c">Go to /a/b/c</Link><br/>
4848
<Form method="post" action="/data">
4949
<button type="submit" name="key" value="value">
5050
Submit
@@ -101,7 +101,7 @@ const files = {
101101
let actionData = useActionData();
102102
return (
103103
<>
104-
<h1 id="heading">Data</h1>
104+
<h1 id="heading">Data Route</h1>
105105
<p id="message">{data.message}</p>
106106
<p id="date">{data.date.toISOString()}</p>
107107
{actionData ? <p id="action-data">{actionData.key}</p> : null}
@@ -1375,6 +1375,45 @@ test.describe("single-fetch", () => {
13751375
expect(await app.getHtml("#target")).toContain("Target");
13761376
});
13771377

1378+
test("supports a basename", async ({ page }) => {
1379+
let fixture = await createFixture({
1380+
files: {
1381+
"vite.config.ts": js`
1382+
import { reactRouter } from "@react-router/dev/vite";
1383+
1384+
export default {
1385+
base: "/base/",
1386+
plugins: [reactRouter()]
1387+
}
1388+
`,
1389+
"react-router.config.ts": reactRouterConfig({
1390+
basename: "/base/",
1391+
}),
1392+
...files,
1393+
},
1394+
useReactRouterServe: true,
1395+
});
1396+
1397+
let appFixture = await createAppFixture(fixture);
1398+
1399+
let requests: string[] = [];
1400+
page.on("request", (req) => {
1401+
let url = new URL(req.url());
1402+
if (url.pathname.endsWith(".data")) {
1403+
requests.push(url.pathname + url.search);
1404+
}
1405+
});
1406+
1407+
let app = new PlaywrightFixture(appFixture, page);
1408+
await app.goto("/base/");
1409+
await app.clickLink("/base/data");
1410+
await expect(page.getByText("Data Route")).toBeVisible();
1411+
await app.clickLink("/base/");
1412+
await expect(page.getByText("Index")).toBeVisible();
1413+
1414+
expect(requests).toEqual(["/base/data.data", "/base/_root.data"]);
1415+
});
1416+
13781417
test("processes redirects when a basename is present", async ({ page }) => {
13791418
let fixture = await createFixture({
13801419
files: {

packages/react-router/lib/dom-export/hydrated-router.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ function createHydratedRouter(): DataRouter {
178178
ssrInfo.manifest,
179179
ssrInfo.routeModules,
180180
ssrInfo.context.ssr,
181+
ssrInfo.context.basename,
181182
() => router
182183
),
183184
patchRoutesOnNavigation: getPatchRoutesOnNavigationFunction(

packages/react-router/lib/dom/ssr/components.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,7 @@ function PrefetchPageLinksImpl({
322322
}) {
323323
let location = useLocation();
324324
let { manifest, routeModules } = useFrameworkContext();
325+
let { basename } = useDataRouterContext();
325326
let { loaderData, matches } = useDataRouterStateContext();
326327

327328
let newMatchesForData = React.useMemo(
@@ -384,7 +385,7 @@ function PrefetchPageLinksImpl({
384385
return [];
385386
}
386387

387-
let url = singleFetchUrl(page);
388+
let url = singleFetchUrl(page, basename);
388389
// When one or more routes have opted out, we add a _routes param to
389390
// limit the loaders to those that have a server loader and did not
390391
// opt out
@@ -400,6 +401,7 @@ function PrefetchPageLinksImpl({
400401

401402
return [url.pathname + url.search];
402403
}, [
404+
basename,
403405
loaderData,
404406
location,
405407
manifest,

packages/react-router/lib/dom/ssr/single-fetch.tsx

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
isRouteErrorResponse,
1414
redirect,
1515
data,
16+
stripBasename,
1617
} from "../../router/utils";
1718
import { createRequestInit } from "./data";
1819
import type { AssetsManifest, EntryContext } from "./entry";
@@ -135,12 +136,13 @@ export function getSingleFetchDataStrategy(
135136
manifest: AssetsManifest,
136137
routeModules: RouteModules,
137138
ssr: boolean,
139+
basename: string | undefined,
138140
getRouter: () => DataRouter
139141
): DataStrategyFunction {
140142
return async ({ request, matches, fetcherKey }) => {
141143
// Actions are simple and behave the same for navigations and fetchers
142144
if (request.method !== "GET") {
143-
return singleFetchActionStrategy(request, matches);
145+
return singleFetchActionStrategy(request, matches, basename);
144146
}
145147

146148
if (!ssr) {
@@ -186,7 +188,7 @@ export function getSingleFetchDataStrategy(
186188
// Skip single fetch and just call the loaders in parallel when this is
187189
// a SPA mode navigation
188190
let matchesToLoad = matches.filter((m) => m.shouldLoad);
189-
let url = stripIndexParam(singleFetchUrl(request.url));
191+
let url = stripIndexParam(singleFetchUrl(request.url, basename));
190192
let init = await createRequestInit(request);
191193
let results: Record<string, DataStrategyResult> = {};
192194
await Promise.all(
@@ -212,7 +214,7 @@ export function getSingleFetchDataStrategy(
212214

213215
// Fetcher loads are singular calls to one loader
214216
if (fetcherKey) {
215-
return singleFetchLoaderFetcherStrategy(request, matches);
217+
return singleFetchLoaderFetcherStrategy(request, matches, basename);
216218
}
217219

218220
// Navigational loads are more complex...
@@ -222,7 +224,8 @@ export function getSingleFetchDataStrategy(
222224
ssr,
223225
getRouter(),
224226
request,
225-
matches
227+
matches,
228+
basename
226229
);
227230
};
228231
}
@@ -231,14 +234,15 @@ export function getSingleFetchDataStrategy(
231234
// navigations and fetchers)
232235
async function singleFetchActionStrategy(
233236
request: Request,
234-
matches: DataStrategyFunctionArgs["matches"]
237+
matches: DataStrategyFunctionArgs["matches"],
238+
basename: string | undefined
235239
) {
236240
let actionMatch = matches.find((m) => m.shouldLoad);
237241
invariant(actionMatch, "No action match found");
238242
let actionStatus: number | undefined = undefined;
239243
let result = await actionMatch.resolve(async (handler) => {
240244
let result = await handler(async () => {
241-
let url = singleFetchUrl(request.url);
245+
let url = singleFetchUrl(request.url, basename);
242246
let init = await createRequestInit(request);
243247
let { data, status } = await fetchAndDecode(url, init);
244248
actionStatus = status;
@@ -272,7 +276,8 @@ async function singleFetchLoaderNavigationStrategy(
272276
ssr: boolean,
273277
router: DataRouter,
274278
request: Request,
275-
matches: DataStrategyFunctionArgs["matches"]
279+
matches: DataStrategyFunctionArgs["matches"],
280+
basename: string | undefined
276281
) {
277282
// Track which routes need a server load - in case we need to tack on a
278283
// `_routes` param
@@ -293,7 +298,7 @@ async function singleFetchLoaderNavigationStrategy(
293298
let singleFetchDfd = createDeferred<SingleFetchResults>();
294299

295300
// Base URL and RequestInit for calls to the server
296-
let url = stripIndexParam(singleFetchUrl(request.url));
301+
let url = stripIndexParam(singleFetchUrl(request.url, basename));
297302
let init = await createRequestInit(request);
298303

299304
// We'll build up this results object as we loop through matches
@@ -418,12 +423,13 @@ async function singleFetchLoaderNavigationStrategy(
418423
// Fetcher loader calls are much simpler than navigational loader calls
419424
async function singleFetchLoaderFetcherStrategy(
420425
request: Request,
421-
matches: DataStrategyFunctionArgs["matches"]
426+
matches: DataStrategyFunctionArgs["matches"],
427+
basename: string | undefined
422428
) {
423429
let fetcherMatch = matches.find((m) => m.shouldLoad);
424430
invariant(fetcherMatch, "No fetcher match found");
425431
let result = await fetcherMatch.resolve(async (handler) => {
426-
let url = stripIndexParam(singleFetchUrl(request.url));
432+
let url = stripIndexParam(singleFetchUrl(request.url, basename));
427433
let init = await createRequestInit(request);
428434
return fetchSingleLoader(handler, url, init, fetcherMatch!.route.id);
429435
});
@@ -462,7 +468,10 @@ function stripIndexParam(url: URL) {
462468
return url;
463469
}
464470

465-
export function singleFetchUrl(reqUrl: URL | string) {
471+
export function singleFetchUrl(
472+
reqUrl: URL | string,
473+
basename: string | undefined
474+
) {
466475
let url =
467476
typeof reqUrl === "string"
468477
? new URL(
@@ -477,6 +486,8 @@ export function singleFetchUrl(reqUrl: URL | string) {
477486

478487
if (url.pathname === "/") {
479488
url.pathname = "_root.data";
489+
} else if (basename && stripBasename(url.pathname, basename) === "/") {
490+
url.pathname = `${basename.replace(/\/$/, "")}/_root.data`;
480491
} else {
481492
url.pathname = `${url.pathname.replace(/\/$/, "")}.data`;
482493
}

packages/react-router/lib/server-runtime/server.ts

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import type { StaticHandler } from "../router/router";
22
import type { ErrorResponse } from "../router/utils";
3-
import { isRouteErrorResponse, ErrorResponseImpl } from "../router/utils";
3+
import {
4+
isRouteErrorResponse,
5+
ErrorResponseImpl,
6+
stripBasename,
7+
} from "../router/utils";
48
import {
59
getStaticContextFromError,
610
createStaticHandler,
@@ -106,12 +110,22 @@ export const createRequestHandler: CreateRequestHandlerFunction = (
106110
}
107111

108112
let url = new URL(request.url);
109-
let normalizedPath = url.pathname
110-
.replace(/\.data$/, "")
111-
.replace(/^\/_root$/, "/");
112-
if (normalizedPath !== "/" && normalizedPath.endsWith("/")) {
113+
114+
let normalizedBasename = _build.basename || "/";
115+
let normalizedPath = url.pathname;
116+
if (stripBasename(normalizedPath, normalizedBasename) === "/_root.data") {
117+
normalizedPath = normalizedBasename;
118+
} else if (normalizedPath.endsWith(".data")) {
119+
normalizedPath = normalizedPath.replace(/\.data$/, "");
120+
}
121+
122+
if (
123+
stripBasename(normalizedPath, normalizedBasename) !== "/" &&
124+
normalizedPath.endsWith("/")
125+
) {
113126
normalizedPath = normalizedPath.slice(0, -1);
114127
}
128+
115129
let params: RouteMatch<ServerRoute>["params"] = {};
116130
let handleError = (error: unknown) => {
117131
if (mode === ServerMode.Development) {
@@ -161,10 +175,7 @@ export const createRequestHandler: CreateRequestHandlerFunction = (
161175
}
162176

163177
// Manifest request for fog of war
164-
let manifestUrl = `${_build.basename ?? "/"}/__manifest`.replace(
165-
/\/+/g,
166-
"/"
167-
);
178+
let manifestUrl = `${normalizedBasename}/__manifest`.replace(/\/+/g, "/");
168179
if (url.pathname === manifestUrl) {
169180
try {
170181
let res = await handleManifestRequest(_build, routes, url);

0 commit comments

Comments
 (0)