Skip to content

Commit ca1f3e7

Browse files
committed
cleanup plumbing, add shared utility for to parsing
1 parent 2df3cfb commit ca1f3e7

File tree

11 files changed

+179
-107
lines changed

11 files changed

+179
-107
lines changed

.changeset/early-doors-obey.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
2-
"@react-router/dev": minor
3-
"react-router": minor
2+
"@react-router/dev": patch
3+
"react-router": patch
44
---
55

66
add support for throwing redirect Response's at RSC render time

integration/rsc/rsc-nojs-test.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,10 +111,15 @@ implementations.forEach((implementation) => {
111111
throw redirect("/render-redirect/redirected");
112112
}
113113
114+
if (id === "external") {
115+
throw redirect("https://example.com/");
116+
}
117+
114118
return (
115119
<>
116120
<h1>{id || "home"}</h1>
117121
<Link to="/render-redirect/redirect">Redirect</Link>
122+
<Link to="/render-redirect/external">External</Link>
118123
</>
119124
)
120125
}
@@ -138,10 +143,15 @@ implementations.forEach((implementation) => {
138143
throw redirect("/render-redirect/lazy/redirected");
139144
}
140145
146+
if (id === "external") {
147+
throw redirect("https://example.com/");
148+
}
149+
141150
return (
142151
<>
143152
<h1>{id || "home"}</h1>
144153
<Link to="/render-redirect/lazy/redirect">Redirect</Link>
154+
<Link to="/render-redirect/external">External</Link>
145155
</>
146156
);
147157
}
@@ -205,13 +215,23 @@ implementations.forEach((implementation) => {
205215
}) => {
206216
await page.goto(`http://localhost:${port}/render-redirect`);
207217
await expect(page.getByText("home")).toBeAttached();
208-
await page.click("a");
218+
await page.getByText("Redirect").click();
209219
await page.waitForURL(
210220
`http://localhost:${port}/render-redirect/redirected`,
211221
);
212222
await expect(page.getByText("redirected")).toBeAttached();
213223
});
214224

225+
test("Suppport throwing external redirect Response from render", async ({
226+
page,
227+
}) => {
228+
await page.goto(`http://localhost:${port}/render-redirect`);
229+
await expect(page.getByText("home")).toBeAttached();
230+
await page.getByText("External").click();
231+
await page.waitForURL(`https://example.com/`);
232+
await expect(page.getByText("Example Domain")).toBeAttached();
233+
});
234+
215235
test("Suppport throwing redirect Response from suspended render", async ({
216236
page,
217237
}) => {
@@ -221,5 +241,13 @@ implementations.forEach((implementation) => {
221241
);
222242
await expect(page.getByText("redirected")).toBeAttached();
223243
});
244+
245+
test("Suppport throwing external redirect Response from suspended render", async ({
246+
page,
247+
}) => {
248+
await page.goto(`http://localhost:${port}/render-redirect/lazy/external`);
249+
await page.waitForURL(`https://example.com/`);
250+
await expect(page.getByText("Example Domain")).toBeAttached();
251+
});
224252
});
225253
});

integration/rsc/rsc-test.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1479,10 +1479,15 @@ implementations.forEach((implementation) => {
14791479
throw redirect("/render-redirect/redirected");
14801480
}
14811481
1482+
if (id === "external") {
1483+
throw redirect("https://example.com/")
1484+
}
1485+
14821486
return (
14831487
<>
14841488
<h1>{id || "home"}</h1>
14851489
<Link to="/render-redirect/redirect">Redirect</Link>
1490+
<Link to="/render-redirect/external">External</Link>
14861491
</>
14871492
)
14881493
}
@@ -1506,10 +1511,15 @@ implementations.forEach((implementation) => {
15061511
throw redirect("/render-redirect/lazy/redirected");
15071512
}
15081513
1514+
if (id === "external") {
1515+
throw redirect("https://example.com/")
1516+
}
1517+
15091518
return (
15101519
<>
15111520
<h1>{id || "home"}</h1>
15121521
<Link to="/render-redirect/lazy/redirect">Redirect</Link>
1522+
<Link to="/render-redirect/external">External</Link>
15131523
</>
15141524
);
15151525
}
@@ -1798,24 +1808,44 @@ implementations.forEach((implementation) => {
17981808
}) => {
17991809
await page.goto(`http://localhost:${port}/render-redirect`);
18001810
await expect(page.getByText("home")).toBeAttached();
1801-
await page.click("a");
1811+
await page.getByText("Redirect").click();
18021812
await page.waitForURL(
18031813
`http://localhost:${port}/render-redirect/redirected`,
18041814
);
18051815
await expect(page.getByText("redirected")).toBeAttached();
18061816
});
18071817

1818+
test("Suppport throwing external redirect Response from render", async ({
1819+
page,
1820+
}) => {
1821+
await page.goto(`http://localhost:${port}/render-redirect`);
1822+
await expect(page.getByText("home")).toBeAttached();
1823+
await page.getByText("External").click();
1824+
await page.waitForURL(`https://example.com/`);
1825+
await expect(page.getByText("Example Domain")).toBeAttached();
1826+
});
1827+
18081828
test("Suppport throwing redirect Response from suspended render", async ({
18091829
page,
18101830
}) => {
18111831
await page.goto(`http://localhost:${port}/render-redirect/lazy`);
18121832
await expect(page.getByText("home")).toBeAttached();
1813-
await page.click("a");
1833+
await page.getByText("Redirect").click();
18141834
await page.waitForURL(
18151835
`http://localhost:${port}/render-redirect/lazy/redirected`,
18161836
);
18171837
await expect(page.getByText("redirected")).toBeAttached();
18181838
});
1839+
1840+
test("Suppport throwing external redirect Response from suspended render", async ({
1841+
page,
1842+
}) => {
1843+
await page.goto(`http://localhost:${port}/render-redirect/lazy`);
1844+
await expect(page.getByText("home")).toBeAttached();
1845+
await page.getByText("External").click();
1846+
await page.waitForURL(`https://example.com/`);
1847+
await expect(page.getByText("Example Domain")).toBeAttached();
1848+
});
18191849
});
18201850

18211851
test.describe("Server Actions", () => {

packages/react-router/lib/components.tsx

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ import {
5454
FetchersContext,
5555
LocationContext,
5656
NavigationContext,
57+
RSCRouterContext,
5758
RouteContext,
5859
ViewTransitionContext,
5960
} from "./context";
@@ -400,12 +401,6 @@ export interface RouterProviderProps {
400401
* For more information, please see the [docs](https://reactrouter.com/explanation/react-transitions).
401402
*/
402403
unstable_useTransitions?: boolean;
403-
404-
/**
405-
* Control whether rsc specific behaviors are enabled. This includes
406-
* `unstable_useTransitions` and redirects thrown at render time.
407-
*/
408-
unstable_rsc?: boolean;
409404
}
410405

411406
/**
@@ -438,16 +433,15 @@ export interface RouterProviderProps {
438433
* @param {RouterProviderProps.unstable_onError} props.unstable_onError n/a
439434
* @param {RouterProviderProps.router} props.router n/a
440435
* @param {RouterProviderProps.unstable_useTransitions} props.unstable_useTransitions n/a
441-
* @param {RouterProviderProps.unstable_rsc} props.unstable_rsc n/a
442436
* @returns React element for the rendered router
443437
*/
444438
export function RouterProvider({
445439
router,
446440
flushSync: reactDomFlushSyncImpl,
447441
unstable_onError,
448442
unstable_useTransitions,
449-
unstable_rsc,
450443
}: RouterProviderProps): React.ReactElement {
444+
let unstable_rsc = React.useContext(RSCRouterContext);
451445
unstable_useTransitions = unstable_useTransitions || unstable_rsc;
452446

453447
let [_state, setStateImpl] = React.useState(router.state);
@@ -728,7 +722,6 @@ export function RouterProvider({
728722
future={router.future}
729723
state={state}
730724
unstable_onError={unstable_onError}
731-
unstable_rsc={unstable_rsc}
732725
/>
733726
</Router>
734727
</ViewTransitionContext.Provider>
@@ -775,22 +768,13 @@ function DataRoutes({
775768
future,
776769
state,
777770
unstable_onError,
778-
unstable_rsc,
779771
}: {
780772
routes: DataRouteObject[];
781773
future: DataRouter["future"];
782774
state: RouterState;
783775
unstable_onError: unstable_ClientOnErrorFunction | undefined;
784-
unstable_rsc: boolean | undefined;
785776
}): React.ReactElement | null {
786-
return useRoutesImpl(
787-
routes,
788-
undefined,
789-
state,
790-
unstable_onError,
791-
unstable_rsc,
792-
future,
793-
);
777+
return useRoutesImpl(routes, undefined, state, unstable_onError, future);
794778
}
795779

796780
/**

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

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,9 @@ function initSsrInfo(): void {
8080
function createHydratedRouter({
8181
getContext,
8282
unstable_instrumentations,
83-
unstable_rsc,
8483
}: {
8584
getContext?: RouterInit["getContext"];
8685
unstable_instrumentations?: unstable_ClientInstrumentation[];
87-
unstable_rsc?: boolean;
8886
}): DataRouter {
8987
initSsrInfo();
9088

@@ -179,9 +177,7 @@ function createHydratedRouter({
179177
hydrationRouteProperties,
180178
unstable_instrumentations,
181179
mapRouteProperties,
182-
future: {
183-
unstable_rsc,
184-
},
180+
future: {},
185181
dataStrategy: getTurboStreamSingleFetchDataStrategy(
186182
() => router,
187183
ssrInfo.manifest,
@@ -319,13 +315,6 @@ export interface HydratedRouterProps {
319315
* For more information, please see the [docs](https://reactrouter.com/explanation/react-transitions).
320316
*/
321317
unstable_useTransitions?: boolean;
322-
323-
/**
324-
* Control whether RSC specific behaviors are introduced. This currently
325-
* enables the unstable_useTransitions flag, as well as the ability to handle
326-
* thrown redirect responses during the render phase.
327-
*/
328-
unstable_rsc?: boolean;
329318
}
330319

331320
/**
@@ -345,7 +334,6 @@ export function HydratedRouter(props: HydratedRouterProps) {
345334
router = createHydratedRouter({
346335
getContext: props.getContext,
347336
unstable_instrumentations: props.unstable_instrumentations,
348-
unstable_rsc: props.unstable_rsc,
349337
});
350338
}
351339

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

Lines changed: 5 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import {
3636
ErrorResponseImpl,
3737
joinPaths,
3838
matchPath,
39+
parseToInfo,
3940
stripBasename,
4041
} from "../router/utils";
4142

@@ -1412,39 +1413,8 @@ export const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(
14121413
React.useContext(NavigationContext);
14131414
let isAbsolute = typeof to === "string" && ABSOLUTE_URL_REGEX.test(to);
14141415

1415-
// Rendered into <a href> for absolute URLs
1416-
let absoluteHref;
1417-
let isExternal = false;
1418-
1419-
if (typeof to === "string" && isAbsolute) {
1420-
// Render the absolute href server- and client-side
1421-
absoluteHref = to;
1422-
1423-
// Only check for external origins client-side
1424-
if (isBrowser) {
1425-
try {
1426-
let currentUrl = new URL(window.location.href);
1427-
let targetUrl = to.startsWith("//")
1428-
? new URL(currentUrl.protocol + to)
1429-
: new URL(to);
1430-
let path = stripBasename(targetUrl.pathname, basename);
1431-
1432-
if (targetUrl.origin === currentUrl.origin && path != null) {
1433-
// Strip the protocol/origin/basename for same-origin absolute URLs
1434-
to = path + targetUrl.search + targetUrl.hash;
1435-
} else {
1436-
isExternal = true;
1437-
}
1438-
} catch (e) {
1439-
// We can't do external URL detection without a valid URL
1440-
warning(
1441-
false,
1442-
`<Link to="${to}"> contains an invalid URL which will probably break ` +
1443-
`when clicked - please update to a valid URL path.`,
1444-
);
1445-
}
1446-
}
1447-
}
1416+
let parsed = parseToInfo(to, basename);
1417+
to = parsed.to;
14481418

14491419
// Rendered into <a href> for relative URLs
14501420
let href = useHref(to, { relative });
@@ -1476,8 +1446,8 @@ export const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(
14761446
<a
14771447
{...rest}
14781448
{...prefetchHandlers}
1479-
href={absoluteHref || href}
1480-
onClick={isExternal || reloadDocument ? onClick : handleClick}
1449+
href={parsed.absoluteURL || href}
1450+
onClick={parsed.isExternal || reloadDocument ? onClick : handleClick}
14811451
ref={mergeRefs(forwardedRef, prefetchRef)}
14821452
target={target}
14831453
data-discover={

packages/react-router/lib/dom/server.tsx

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -232,21 +232,12 @@ function DataRoutes({
232232
routes,
233233
future,
234234
state,
235-
unstable_rsc,
236235
}: {
237236
routes: DataRouteObject[];
238237
future: DataRouter["future"];
239238
state: RouterState;
240-
unstable_rsc?: boolean;
241239
}): React.ReactElement | null {
242-
return useRoutesImpl(
243-
routes,
244-
undefined,
245-
state,
246-
undefined,
247-
unstable_rsc,
248-
future,
249-
);
240+
return useRoutesImpl(routes, undefined, state, undefined, future);
250241
}
251242

252243
function serializeErrors(

packages/react-router/lib/errors.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,33 @@ const ERROR_DIGEST_REDIRECT = "REDIRECT";
44
export function createRedirectErrorDigest(response: Response) {
55
return `${ERROR_DIGEST_BASE}:${ERROR_DIGEST_REDIRECT}:${JSON.stringify({
66
status: response.status,
7+
statusText: response.statusText,
78
location: response.headers.get("Location"),
9+
reloadDocument: response.headers.get("X-Remix-Reload-Document") === "true",
10+
replace: response.headers.get("X-Remix-Replace") === "true",
811
})}`;
912
}
1013

11-
export function decodeRedirectErrorDigest(
12-
digest: string,
13-
): undefined | { status: number; location: string } {
14+
export function decodeRedirectErrorDigest(digest: string):
15+
| undefined
16+
| {
17+
status: number;
18+
statusText: string;
19+
location: string;
20+
reloadDocument: boolean;
21+
replace: boolean;
22+
} {
1423
if (digest.startsWith(`${ERROR_DIGEST_BASE}:${ERROR_DIGEST_REDIRECT}:{`)) {
1524
try {
1625
let parsed = JSON.parse(digest.slice(28));
1726
if (
1827
typeof parsed === "object" &&
1928
parsed &&
20-
"status" in parsed &&
2129
typeof parsed.status === "number" &&
22-
"location" in parsed &&
23-
typeof parsed.location === "string"
30+
typeof parsed.statusText === "string" &&
31+
typeof parsed.location === "string" &&
32+
typeof parsed.reloadDocument === "boolean" &&
33+
typeof parsed.replace === "boolean"
2434
) {
2535
return parsed;
2636
}

0 commit comments

Comments
 (0)