@@ -722,7 +722,20 @@ export default function vitePluginRsc(
722722 async hotUpdate ( ctx ) {
723723 if ( isCSSRequest ( ctx . file ) ) {
724724 if ( this . environment . name === 'client' ) {
725- return
725+ const cssLinkPrecedence = rscPluginOptions . cssLinkPrecedence ?? true
726+ if ( cssLinkPrecedence ) return
727+
728+ // Without stylesheet precedence, React owns swapping stylesheet link
729+ // for server css hmr by reconciling new link with <link href="...?t=..." >,
730+ // so we filter out `css` type to prevent triggering Vite's `css-update` hmr,
731+ // which tries to swap the same link.
732+ // we keep `js` type hmr to trigger hmr for css side effect import on client environment
733+ // (though probably css imported both client and server don't behave well.)
734+ const rscMod =
735+ ctx . server . environments . rsc ?. moduleGraph . getModuleById ( ctx . file )
736+ if ( rscMod ) {
737+ return ctx . modules . filter ( ( mod ) => mod . type !== 'css' )
738+ }
726739 }
727740 }
728741
@@ -2229,7 +2242,7 @@ function vitePluginRscCss(
22292242 const visitedFiles = new Set < string > ( )
22302243
22312244 function recurse ( id : string ) {
2232- if ( visited . has ( id ) ) {
2245+ if ( visited . has ( id ) || parseCssVirtual ( id ) ) {
22332246 return
22342247 }
22352248 visited . add ( id )
@@ -2240,6 +2253,9 @@ function vitePluginRscCss(
22402253 for ( const next of mod ?. importedModules ?? [ ] ) {
22412254 if ( next . id ) {
22422255 if ( isCSSRequest ( next . id ) ) {
2256+ if ( next . file ) {
2257+ visitedFiles . add ( next . file )
2258+ }
22432259 if ( hasSpecialCssQuery ( next . id ) ) {
22442260 continue
22452261 }
@@ -2253,9 +2269,32 @@ function vitePluginRscCss(
22532269
22542270 recurse ( entryId )
22552271
2256- // this doesn't include ?t= query so that RSC <link /> won't keep adding styles.
2272+ // CSS links emitted from RSC participate in two different HMR strategies.
2273+ //
2274+ // cssLinkPrecedence: true (default)
2275+ // React treats <link precedence="..."> as a stylesheet resource. Keep the
2276+ // RSC-rendered href stable here and let Vite's normal client `css-update`
2277+ // handle edits for the `.css?direct` module. Vite finds the existing
2278+ // stylesheet link by pathname, clones it, rewrites the clone to
2279+ // `?t=<timestamp>`, then removes the previous link after the new one
2280+ // loads. https://github.com/vitejs/vite/blob/a19003516951a3710aab0f2646d78c48b2e5d2ad/packages/vite/src/client/client.ts#L234-L235
2281+ // React does not re-insert that original stable-href resource on
2282+ // later RSC renders, so the Vite-owned timestamped link remains the live
2283+ // stylesheet. This is why RSC must not inject its own timestamp in
2284+ // precedence mode: doing so makes React see every edit as a new resource
2285+ // and append more stylesheet links.
2286+ //
2287+ // cssLinkPrecedence: false
2288+ // React reconciles <link> like a normal DOM element. In this mode RSC owns
2289+ // the link swap: on CSS HMR, the RSC refetch re-renders this resource list
2290+ // with `?t=<lastHMRTimestamp>`.
2291+ // In this mode, we prevent Vite from swapping the same style
2292+ // by filtering out `css-update` hmr in our `rsc` plugin's `hotUpdate` hook above.
2293+ const cssLinkPrecedence = rscCssOptions ?. cssLinkPrecedence ?? true
22572294 const hrefs = [ ...cssIds ] . map ( ( id ) =>
2258- normalizeViteImportAnalysisUrl ( environment , id ) ,
2295+ normalizeViteImportAnalysisUrl ( environment , id , {
2296+ injectHMRTimestamp : ! cssLinkPrecedence ,
2297+ } ) ,
22592298 )
22602299 return { ids : [ ...cssIds ] , hrefs, visitedFiles : [ ...visitedFiles ] }
22612300 }
0 commit comments