diff --git a/packages/plugin-rsc/e2e/css-code-split.test.ts b/packages/plugin-rsc/e2e/css-code-split.test.ts
new file mode 100644
index 000000000..09dbe4842
--- /dev/null
+++ b/packages/plugin-rsc/e2e/css-code-split.test.ts
@@ -0,0 +1,69 @@
+import { expect, test } from '@playwright/test'
+import { setupInlineFixture, useFixture } from './fixture'
+import { defineStarterTest } from './starter'
+
+test.describe('cssCodeSplit-false', () => {
+ const root = 'examples/e2e/temp/cssCodeSplit-false'
+
+ test.beforeAll(async () => {
+ await setupInlineFixture({
+ src: 'examples/starter',
+ dest: root,
+ files: {
+ 'vite.config.base.ts': { cp: 'vite.config.ts' },
+ 'vite.config.ts': /* js */ `
+ import { defineConfig, mergeConfig } from 'vite'
+ import baseConfig from './vite.config.base.ts'
+
+ const overrideConfig = defineConfig({
+ build: {
+ cssCodeSplit: false,
+ },
+ })
+
+ export default mergeConfig(baseConfig, overrideConfig)
+ `,
+ // test server css module too
+ // (starter example already tests normal server css)
+ 'src/server-only.module.css': /* css */ `
+ .serverOnly {
+ color: rgb(123, 45, 67);
+ }
+ `,
+ 'src/server-only.tsx': /* js */ `
+ import styles from './server-only.module.css'
+ export function ServerOnly() {
+ return (
+
+ )
+ }
+ `,
+ 'src/root.tsx': {
+ edit: (s) =>
+ s
+ .replace(
+ `import { ClientCounter } from './client.tsx'`,
+ `import { ClientCounter } from './client.tsx';
+ import { ServerOnly } from './server-only.tsx'`,
+ )
+ .replace(``, ``),
+ },
+ },
+ })
+ })
+
+ test.describe('build', () => {
+ const f = useFixture({ root, mode: 'build' })
+ defineStarterTest(f)
+
+ test('server css module', async ({ page }) => {
+ await page.goto(f.url())
+ await expect(page.getByTestId('server-only')).toHaveCSS(
+ 'color',
+ 'rgb(123, 45, 67)',
+ )
+ })
+ })
+})
diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts
index 9141febba..ba546297a 100644
--- a/packages/plugin-rsc/src/plugin.ts
+++ b/packages/plugin-rsc/src/plugin.ts
@@ -1032,6 +1032,21 @@ export function createRpcClient(params) {
},
},
},
+ {
+ // keep track of build output for each environment build to generate
+ // assets manaifest and environment imports manifest.
+ // use `order: "post"` to include all chunks/assets
+ // generated by other plugins
+ // (e.g. `styles.css` generated by `vite:css-post` when `cssCodeSplit: false`).
+ // https://github.com/vitejs/vite/blob/a19003516951a3710aab0f2646d78c48b2e5d2ad/packages/vite/src/node/plugins/css.ts#L1040
+ name: 'rsc:virtual:vite-rsc/track-bundles',
+ generateBundle: {
+ order: 'post',
+ handler(_options, bundle) {
+ manager.bundles[this.environment.name] = bundle
+ },
+ },
+ },
{
name: 'rsc:virtual:vite-rsc/assets-manifest',
resolveId: {
@@ -1067,10 +1082,31 @@ export function createRpcClient(params) {
// client build
generateBundle(_options, bundle) {
// copy assets from rsc build to client build
- manager.bundles[this.environment.name] = bundle
-
if (this.environment.name === 'client') {
const rscBundle = manager.bundles['rsc']!
+
+ // when css code split is disabled, treat vite's single css bundle `style.css`
+ // as dependency of all server chunks
+ if (!manager.config.environments.rsc!.build.cssCodeSplit) {
+ let cssBundleName: string | undefined
+ for (const output of Object.values(rscBundle)) {
+ if (
+ output.type === 'asset' &&
+ output.names.includes('style.css')
+ ) {
+ cssBundleName = output.fileName
+ break
+ }
+ }
+ if (cssBundleName) {
+ for (const output of Object.values(rscBundle)) {
+ if (output.type === 'chunk') {
+ output.viteMetadata!.importedCss.add(cssBundleName)
+ }
+ }
+ }
+ }
+
const assets = new Set(
Object.values(rscBundle).flatMap((output) =>
output.type === 'chunk'
@@ -1092,7 +1128,7 @@ export function createRpcClient(params) {
}
const serverResources: Record = {}
- const rscAssetDeps = collectAssetDeps(manager.bundles['rsc']!)
+ const rscAssetDeps = collectAssetDeps(rscBundle)
for (const [id, meta] of Object.entries(
manager.serverResourcesMetaMap,
)) {