Repository navigation
plugin-rsc: a server component's CSS is not linked in production when its module has no final newline #1495
Description
Related plugins
Describe the bug
Hi, and thank you for plugin-rsc! It made it possible for us to run the Payload CMS 4 admin panel on React Router 8's RSC Framework Mode, and the automatic CSS injection for server components has been a pleasure to rely on.
We found one case where that injection silently does nothing, in production builds only: when a server component's module has no final newline and ends with a line comment, its CSS file is built and listed in the assets manifest, but no <link> reaches the page.
How we found it. Our admin pages rendered unstyled in production while vite dev looked fine. The affected parts were all server components from @payloadcms/ui: the login and create-first-user templates (the form spread over the full viewport width instead of its centered 400px column), the navigation, and the account settings. On the login page:
- the production build emitted 12 CSS files, and the page linked only 6 of them;
- the ones not linked included
Root-*.css(which holds the.template-minimalrules),Default-*.css,adminViews-*.cssandVerify-*.css, although__vite_rsc_assets_manifest.jscorrectly mapstemplates/Minimal/index.jstoRoot-*.css; - in the RSC chunk,
MinimalTemplatewas never wrapped: the wrapper assignment sat inside a comment.
Every compiled file of @payloadcms/ui ends with //# sourceMappingURL=index.js.map and no final newline. transformRscCssExport turned the end of templates/Minimal/index.js into:
//# sourceMappingURL=index.js.mapMinimalTemplate = /* #__PURE__ */ __vite_rsc_wrap_css__(MinimalTemplate, "MinimalTemplate");
export { MinimalTemplate };Cause. transformWrapExport moves the rewritten export declaration to the end of the module (output.move(start, end, input.length)), and the moved code starts without a newline. When the input has no final newline, the moved code joins the last line. If that line is a comment, the __vite_rsc_wrap_css__(...) call becomes part of it, never runs, and loadCss() is never rendered for that component. vite dev is not affected because it collects CSS from the module graph.
I have opened #1494 with a fix: it adds a newline before the first moved block, only when the input does not end with one, so modules that already end with a newline produce the same output as before. I hope that is useful, and I am happy to change the approach if you prefer another one.
Reproduction
https://github.com/Mheaus/vite-plugin-rsc-final-newline-repro
Steps to reproduce
The reproduction starts from examples/starter and adds two local packages that look like a compiled UI library. Each exports a server component that imports ./card.css. They are identical except for the last byte of dist/index.js:
| Package | dist/index.js ends with |
|---|---|
compiled-card |
//# sourceMappingURL=index.js.map without a final newline |
source-card |
the same comment with a final newline |
Both are loaded with await import() so that each gets its own chunk and CSS file, and the RSC build bundles them (resolve.noExternal), as a framework does for UI packages that import CSS.
pnpm install
./check.sh # vite build, vite preview, then inspects the served HTMLResult with @vitejs/plugin-rsc 0.5.35 and 0.5.36 (vite 8.3.4):
compiled-card: css built=dist-S79n46Dc.css linked=0 rendered=1
source-card: css built=dist-CEWtfTe8.css linked=1 rendered=1
Both cards render, but only source-card gets its stylesheet. With the change from #1494 applied to the installed plugin, both are linked:
compiled-card: css built=dist-S79n46Dc.css linked=1 rendered=1
source-card: css built=dist-CEWtfTe8.css linked=1 rendered=1
Some notes from investigating, in case they help:
- A module inside the project (
.tsxor.jsundersrc/) does not reproduce it: the comment is gone beforersc:rsc-css-export-transformsees the code. It reproduces with files that come from a package innode_modules, which is where compiled libraries live. "use server"modules go throughtransformModuleExportEffect, which already surrounds moved code with newlines (\n${code}\n), so they do not have this problem.- Until a release ships, a small
enforce: 'pre'plugin that appends\nto modules without a final newline in therscenvironment works around it. That is what we ship in our adapter today.
System Info
System:
OS: macOS 27.0
CPU: (18) arm64 Apple M5 Pro
Memory: 1.45 GB / 48.00 GB
Shell: 5.9 - /bin/zsh
Binaries:
Node: 26.5.0 - /opt/homebrew/bin/node
npm: 11.17.0 - /opt/homebrew/bin/npm
pnpm: 11.17.0
Browsers:
Chrome: 154.0.8037.98
Firefox: 153.0
Safari: 27.0
npmPackages:
@vitejs/plugin-react: 6.0.1 => 6.0.1
@vitejs/plugin-rsc: 0.5.36 => 0.5.36
vite: 8.3.4 => 8.3.4Also reproduced on Node 24.15.0.
Used Package Manager
pnpm
Logs
No error is logged: the build succeeds and the page renders, only without the component's stylesheet.
Validations
- Follow our Code of Conduct
- Read the Contributing Guidelines.
- Read the docs.
- Check that there isn't already an issue that reports the same bug to avoid creating a duplicate.
- Make sure this is a Vite issue and not a framework-specific issue.
Thanks a lot for your time!
Activity
fix(rsc): keep CSS wrappers out of a trailing comment when a module h…