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, )) {