Skip to content

plugin-rsc: a server component's CSS is not linked in production when its module has no final newline #1495

Description

@Mheaus

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-minimal rules), Default-*.css, adminViews-*.css and Verify-*.css, although __vite_rsc_assets_manifest.js correctly maps templates/Minimal/index.js to Root-*.css;
  • in the RSC chunk, MinimalTemplate was 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 HTML

Result 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 (.tsx or .js under src/) does not reproduce it: the comment is gone before rsc:rsc-css-export-transform sees the code. It reproduces with files that come from a package in node_modules, which is where compiled libraries live.
  • "use server" modules go through transformModuleExportEffect, 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 \n to modules without a final newline in the rsc environment 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.4

Also 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

Thanks a lot for your time!

Activity

  1. added a commit that references this issue on Oct 13, 2026
    9a68f29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions