Skip to content

Commit 3114e88

Browse files
authored
fix(rsc): remove duplicate server css on initial render (#702)
1 parent a1f4311 commit 3114e88

File tree

4 files changed

+69
-5
lines changed

4 files changed

+69
-5
lines changed

packages/plugin-rsc/e2e/basic.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,22 @@ function defineTest(f: Fixture) {
473473
'color',
474474
'rgb(255, 165, 0)',
475475
)
476+
await expectNoDuplicateServerCss(page)
477+
})
478+
479+
async function expectNoDuplicateServerCss(page: Page) {
480+
// check only manually inserted stylesheet link exists
481+
// (toHaveAttribute passes only when locator matches single element)
482+
await expect(page.locator('link[rel="stylesheet"]')).toHaveAttribute(
483+
'href',
484+
'/test-style-server-manual.css',
485+
)
486+
}
487+
488+
test('no duplicate server css', async ({ page }) => {
489+
await page.goto(f.url())
490+
await waitForHydration(page)
491+
await expectNoDuplicateServerCss(page)
476492
})
477493

478494
test('adding/removing css client @js', async ({ page }) => {
@@ -557,6 +573,7 @@ function defineTest(f: Fixture) {
557573
'color',
558574
'rgb(255, 165, 0)',
559575
)
576+
await expectNoDuplicateServerCss(page)
560577
})
561578

562579
// TODO: need a way to add/remove links on server hmr. for now, it requires a manually reload.

packages/plugin-rsc/examples/basic/src/routes/style-server/server.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export function TestStyleServer() {
1010
</div>
1111
<link
1212
rel="stylesheet"
13-
href="/test.css"
13+
href="/test-style-server-manual.css"
1414
precedence="test-style-server-manual"
1515
/>
1616
<div className="test-style-server-manual">test-style-server-manual</div>

packages/plugin-rsc/src/plugin.ts

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -837,7 +837,8 @@ window.__vite_plugin_react_preamble_installed__ = true;
837837
const resolvedEntry = await this.resolve(source)
838838
assert(resolvedEntry, `[vite-rsc] failed to resolve entry '${source}'`)
839839
code += `await import(${JSON.stringify(resolvedEntry.id)});`
840-
// TODO: this doesn't have to wait for "vite:beforeUpdate" and should do it right after browser css import.
840+
// server css is normally removed via `RemoveDuplicateServerCss` on useEffect.
841+
// this also makes sure they are removed on hmr in case initial rendering failed.
841842
code += /* js */ `
842843
const ssrCss = document.querySelectorAll("link[rel='stylesheet']");
843844
import.meta.hot.on("vite:beforeUpdate", () => {
@@ -1910,6 +1911,36 @@ export function vitePluginRscCss(
19101911
}
19111912
},
19121913
},
1914+
createVirtualPlugin(
1915+
'vite-rsc/remove-duplicate-server-css',
1916+
async function () {
1917+
// Remove duplicate css during dev due to server rendered <link> and client inline <style>
1918+
// https://github.com/remix-run/react-router/blob/166fd940e7d5df9ed005ca68e12de53b1d88324a/packages/react-router/lib/dom-export/hydrated-router.tsx#L245-L274
1919+
assert.equal(this.environment.mode, 'dev')
1920+
function removeFn() {
1921+
document
1922+
.querySelectorAll("link[rel='stylesheet']")
1923+
.forEach((node) => {
1924+
if (
1925+
node instanceof HTMLElement &&
1926+
node.dataset.precedence?.startsWith('vite-rsc/')
1927+
) {
1928+
node.remove()
1929+
}
1930+
})
1931+
}
1932+
return `\
1933+
"use client"
1934+
import React from "react";
1935+
export default function RemoveDuplicateServerCss() {
1936+
React.useEffect(() => {
1937+
(${removeFn.toString()})();
1938+
}, []);
1939+
return null;
1940+
}
1941+
`
1942+
},
1943+
),
19131944
]
19141945
}
19151946

@@ -1940,6 +1971,7 @@ function generateResourcesCode(depsCode: string) {
19401971
const ResourcesFn = (
19411972
React: typeof import('react'),
19421973
deps: ResolvedAssetDeps,
1974+
RemoveDuplicateServerCss?: React.FC,
19431975
) => {
19441976
return function Resources() {
19451977
return React.createElement(React.Fragment, null, [
@@ -1960,14 +1992,29 @@ function generateResourcesCode(depsCode: string) {
19601992
src: href,
19611993
}),
19621994
),
1995+
RemoveDuplicateServerCss &&
1996+
React.createElement(RemoveDuplicateServerCss, {
1997+
key: 'remove-duplicate-css',
1998+
}),
19631999
])
19642000
}
19652001
}
19662002

19672003
return `
1968-
import __vite_rsc_react__ from "react";
1969-
export const Resources = (${ResourcesFn.toString()})(__vite_rsc_react__, ${depsCode});
1970-
`
2004+
import __vite_rsc_react__ from "react";
2005+
2006+
${
2007+
config.command === 'serve'
2008+
? `import RemoveDuplicateServerCss from "virtual:vite-rsc/remove-duplicate-server-css";`
2009+
: `const RemoveDuplicateServerCss = undefined;`
2010+
}
2011+
2012+
export const Resources = (${ResourcesFn.toString()})(
2013+
__vite_rsc_react__,
2014+
${depsCode},
2015+
RemoveDuplicateServerCss,
2016+
);
2017+
`
19712018
}
19722019

19732020
export async function transformRscCssExport(options: {

0 commit comments

Comments
 (0)