Skip to content

Commit 300927d

Browse files
Improve CSS HMR in RSC Framework Mode (#14219)
1 parent 4fec1d5 commit 300927d

File tree

9 files changed

+101
-69
lines changed

9 files changed

+101
-69
lines changed

integration/helpers/rsc-parcel/src/routes/root.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export function Layout({ children }: { children: React.ReactNode }) {
66
<head>
77
<meta charSet="utf-8" />
88
<meta name="viewport" content="width=device-width, initial-scale=1" />
9-
<title>Vite (RSC)</title>
9+
<title>Parcel (RSC)</title>
1010
<Links />
1111
</head>
1212
<body>

integration/helpers/rsc-vite-framework/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
"@vanilla-extract/css": "^1.17.4",
2222
"@vanilla-extract/vite-plugin": "^5.1.1",
2323
"@vitejs/plugin-react": "^4.5.2",
24-
"@vitejs/plugin-rsc": "0.4.11",
24+
"@vitejs/plugin-rsc": "0.4.21",
2525
"cross-env": "^7.0.3",
2626
"typescript": "^5.1.6",
2727
"vite": "^6.2.0",

integration/helpers/rsc-vite/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"typecheck": "tsc"
1111
},
1212
"devDependencies": {
13-
"@vitejs/plugin-rsc": "0.4.11",
13+
"@vitejs/plugin-rsc": "0.4.21",
1414
"@types/express": "^5.0.0",
1515
"@types/node": "^22.13.1",
1616
"@types/react": "^19.1.8",

integration/vite-css-test.ts

Lines changed: 66 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,18 @@ const fixtures = [
3434
templateDisplayName: string;
3535
}>;
3636

37-
type RouteBasePath = "css" | "rsc-server-first-css";
37+
type RouteBasePath =
38+
| "css-with-links-export"
39+
| "css-with-floated-link"
40+
| "rsc-server-first-route";
3841
const getRouteBasePaths = (templateName: TemplateName) => {
39-
if (templateName.includes("rsc")) {
40-
return ["css", "rsc-server-first-css"] as const satisfies RouteBasePath[];
41-
}
42-
return ["css"] as const satisfies RouteBasePath[];
42+
return [
43+
"css-with-links-export",
44+
"css-with-floated-link",
45+
...(templateName.includes("rsc")
46+
? (["rsc-server-first-route"] as const)
47+
: []),
48+
] as const satisfies RouteBasePath[];
4349
};
4450

4551
const files = ({ templateName }: { templateName: TemplateName }) => ({
@@ -112,7 +118,7 @@ const files = ({ templateName }: { templateName: TemplateName }) => ({
112118
...Object.assign(
113119
{},
114120
...getRouteBasePaths(templateName).map((routeBasePath) => {
115-
const isServerFirstRoute = routeBasePath === "rsc-server-first-css";
121+
const isServerFirstRoute = routeBasePath === "rsc-server-first-route";
116122
const exportName = isServerFirstRoute ? "ServerComponent" : "default";
117123

118124
return {
@@ -162,14 +168,17 @@ const files = ({ templateName }: { templateName: TemplateName }) => ({
162168
return null;
163169
}
164170
165-
export function links() {
166-
return [{ rel: "stylesheet", href: postcssLinkedStyles }];
171+
${
172+
routeBasePath === "css-with-links-export"
173+
? `export function links() { return [{ rel: "stylesheet", href: postcssLinkedStyles }]; }`
174+
: ""
167175
}
168176
169177
function TestRoute() {
170178
return (
171179
<>
172180
<input />
181+
${routeBasePath !== "css-with-links-export" ? `<link rel="stylesheet" href={postcssLinkedStyles} precedence="default" />` : ""}
173182
<div id="entry-client" className="entry-client">
174183
<div id="css-modules" className={cssModulesStyles.index}>
175184
<div id="css-postcss-linked" className="${routeBasePath}-postcss-linked">
@@ -517,11 +526,6 @@ async function hmrWorkflow({
517526
base?: string;
518527
templateName: TemplateName;
519528
}) {
520-
if (templateName.includes("rsc")) {
521-
// TODO: Fix CSS HMR support in RSC Framework mode
522-
return;
523-
}
524-
525529
for (const routeBase of getRouteBasePaths(templateName)) {
526530
let pageErrors: Error[] = [];
527531
page.on("pageerror", (error) => pageErrors.push(error));
@@ -532,8 +536,6 @@ async function hmrWorkflow({
532536

533537
let input = page.locator("input");
534538
await expect(input).toBeVisible();
535-
await input.type("stateful");
536-
await expect(input).toHaveValue("stateful");
537539

538540
let edit = createEditor(cwd);
539541
let modifyCss = (contents: string) =>
@@ -544,34 +546,57 @@ async function hmrWorkflow({
544546
"NEW_PADDING_INJECTED_VIA_POSTCSS",
545547
);
546548

547-
await Promise.all([
548-
edit(`app/routes/${routeBase}/styles-bundled.css`, modifyCss),
549-
edit(`app/routes/${routeBase}/styles.module.css`, modifyCss),
550-
edit(`app/routes/${routeBase}/styles-vanilla-global.css.ts`, modifyCss),
551-
edit(`app/routes/${routeBase}/styles-vanilla-local.css.ts`, modifyCss),
552-
edit(`app/routes/${routeBase}/styles-postcss-linked.css`, modifyCss),
553-
]);
554-
555-
await Promise.all(
556-
[
557-
"#css-bundled",
558-
"#css-postcss-linked",
559-
"#css-modules",
560-
"#css-vanilla-global",
561-
"#css-vanilla-local",
562-
].map(
563-
async (selector) =>
564-
await expect(page.locator(selector)).toHaveCSS(
565-
"padding",
566-
NEW_PADDING,
567-
),
568-
),
569-
);
549+
const testCases = [
550+
{ file: "styles-bundled.css", selector: "#css-bundled" },
551+
// TODO: Fix HMR for CSS Modules in server-first routes in RSC Framework mode
552+
...(routeBase === "rsc-server-first-route"
553+
? []
554+
: [{ file: "styles.module.css", selector: "#css-modules" }]),
555+
// TODO: Fix HMR for `?url` CSS imports in RSC Framework mode: https://github.com/vitejs/vite-plugin-react/issues/772
556+
// Once fixed, check if this also fixes HMR for Vanilla Extract
557+
...(templateName.includes("rsc")
558+
? []
559+
: [
560+
{
561+
file: "styles-postcss-linked.css",
562+
selector: "#css-postcss-linked",
563+
},
564+
{
565+
file: "styles-vanilla-global.css.ts",
566+
selector: "#css-vanilla-global",
567+
},
568+
{
569+
file: "styles-vanilla-local.css.ts",
570+
selector: "#css-vanilla-local",
571+
},
572+
]),
573+
] as const satisfies Array<{
574+
file: string;
575+
selector: string;
576+
}>;
577+
578+
for (const { file, selector } of testCases) {
579+
const routeFile = `app/routes/${routeBase}/${file}`;
580+
await input.fill(routeFile);
581+
await edit(routeFile, modifyCss);
582+
await expect(
583+
page.locator(selector),
584+
`CSS update for ${routeFile}`,
585+
).toHaveCSS("padding", NEW_PADDING);
586+
587+
// TODO: Fix state preservation when changing CSS Modules in RSC Framework mode
588+
if (templateName.includes("rsc") && file === "styles.module.css") {
589+
continue;
590+
}
570591

571-
// Ensure CSS updates were handled by HMR
572-
await expect(input).toHaveValue("stateful");
592+
// Ensure CSS updates were handled by HMR
593+
await expect(input, `State preservation for ${routeFile}`).toHaveValue(
594+
routeFile,
595+
);
596+
}
573597

574-
if (routeBase === "css") {
598+
// RSC Framework mode doesn't support custom entries yet
599+
if (!templateName.includes("rsc")) {
575600
// The following change triggers a full page reload, so we check it after all the checks for HMR state preservation
576601
await edit("app/entry.client.css", modifyCss);
577602
await expect(page.locator("#entry-client")).toHaveCSS(

packages/react-router-dev/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@
7878
"@babel/types": "^7.27.7",
7979
"@npmcli/package-json": "^4.0.1",
8080
"@react-router/node": "workspace:*",
81-
"@vitejs/plugin-rsc": "0.4.11",
81+
"@vitejs/plugin-rsc": "0.4.21",
8282
"arg": "^5.0.1",
8383
"babel-dead-code-elimination": "^1.0.6",
8484
"chokidar": "^4.0.0",

packages/react-router/lib/rsc/server.ssr.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,8 @@ export async function routeRSCServerRequest({
112112
throw new Error("Missing body in server response");
113113
}
114114

115+
const detectRedirectResponse = serverResponse.clone();
116+
115117
let serverResponseB: Response | null = null;
116118
if (hydrate) {
117119
serverResponseB = serverResponse.clone();
@@ -126,7 +128,12 @@ export async function routeRSCServerRequest({
126128
};
127129

128130
try {
129-
const payload = await getPayload();
131+
if (!detectRedirectResponse.body) {
132+
throw new Error("Failed to clone server response");
133+
}
134+
const payload = (await createFromReadableStream(
135+
detectRedirectResponse.body,
136+
)) as RSCPayload;
130137
if (
131138
serverResponse.status === SINGLE_FETCH_REDIRECT_STATUS &&
132139
payload.type === "redirect"

playground/rsc-vite-framework/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
"@types/react": "^19.1.8",
1919
"@types/react-dom": "^19.1.6",
2020
"@vitejs/plugin-react": "^4.5.2",
21-
"@vitejs/plugin-rsc": "0.4.11",
21+
"@vitejs/plugin-rsc": "0.4.21",
2222
"cross-env": "^7.0.3",
2323
"remark-frontmatter": "^5.0.0",
2424
"remark-mdx-frontmatter": "^5.2.0",

playground/rsc-vite/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"@types/react": "^19.1.8",
1616
"@types/react-dom": "^19.1.6",
1717
"@vitejs/plugin-react": "^4.5.2",
18-
"@vitejs/plugin-rsc": "0.4.11",
18+
"@vitejs/plugin-rsc": "0.4.21",
1919
"cross-env": "^7.0.3",
2020
"typescript": "^5.1.6",
2121
"vite": "^6.2.0"

pnpm-lock.yaml

Lines changed: 21 additions & 21 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)