fix(build): emit SSR CSS assets referenced by SSR entry - #1304
Conversation
Set `build.ssrEmitAssets: true` so the SSR environment writes asset files (CSS, etc.) that JS chunks reference. Vite defaults `emitAssets` to `false` for environments with `consumer: "server"`, so when the CSS plugin rewrites a server-component CSS import into `import "<hash>.css"` and emits the CSS asset, the asset file is subsequently stripped from the bundle by Vite's asset-cleanup hook. The dangling import then crashes `vinext start` with ERR_MODULE_NOT_FOUND on `dist/server/ssr/style.css`. We set the top-level `build.ssrEmitAssets` rather than `environments.ssr.build.emitAssets` because Vite re-applies the top-level value during `resolveConfig` and would otherwise overwrite any per-environment value. Mirrors `@vitejs/plugin-rsc`'s existing `emitAssets: true` on the `rsc` environment. Adds tests asserting both the App Router and Pages Router SSR environments resolve to `emitAssets: true` and that every CSS import in the SSR build output resolves to a real file on disk.
commit: |
|
|
/bigbonk review |
There was a problem hiding this comment.
LGTM. The fix is correct, well-scoped, and thoroughly tested.
Verification
Root cause confirmed in Vite source. I traced the exact code path in the installed @voidzero-dev/vite-plus-core:
node.js:38430—emitAssetsdefaults toconsumer === "client", sofalsefor the SSR envnode.js:39884-39887—config.build.ssrEmitAssetspropagates intoconfigEnvironmentsSsr.build.emitAssetsduring early env config mergingnode.js:40108— A late override appliesresolved.build.ssrEmitAssets || resolved.build.emitAssetstoenvironments.ssr, but only when!resolved.builder?.sharedConfigBuild
Why build.ssrEmitAssets is the right form:
@vitejs/plugin-rscsetsbuilder.sharedConfigBuild: true(plugin-rscplugin-BhzHKRFo.js:737), which disables the late override at step 3- But step 2 still runs regardless of
sharedConfigBuild, sossrEmitAssets: truecorrectly propagates in both the App Router path (sharedConfigBuild=true) and the Pages Router path (sharedConfigBuild=false/undefined) - A per-environment
environments.ssr.build.emitAssetswould work for App Router (because sharedConfigBuild skips the late override), but would be clobbered by the late override in the Pages Router path — exactly as the PR description explains
@vitejs/plugin-rsc only covers rsc, not ssr: Confirmed at plugin-BhzHKRFo.js:715 — the RSC plugin sets emitAssets: true on the rsc environment config but not on ssr. This PR fills that gap.
Tests
All three tests pass locally (1.15s). The test structure is sound:
- Config-level assertions for both App Router and Pages Router SSR envs confirm the setting propagates
- The integration test (full
createBuilder+buildApp) verifies the end-to-end behavior — everyimport "*.css"in SSR chunks resolves to a file on disk
The fixture construction is clean (tmpdir + symlinked node_modules + cleanup in finally blocks).
No concerns.
|
Reviewed and approved PR #1304. The fix is correct —
|
Problem
Production deployments fail at
vinext starttime when a server component (or any module SSR'd through the SSR environment) imports a CSS file:Root cause
Vite defaults
environments.ssr.build.emitAssetstofalsefor environments withconsumer: "server"(the SSR env's default). With code-split CSS (Vite's default), Vite's CSS plugin still:import "<hash>.css"statements in the SSR JS chunkemitFileBut Vite's
vite:assetgenerateBundlehook then strips all asset files from the bundle becauseemitAssetsis false. The danglingimportthen crashes Node's ESM loader atvinext starttime.@vitejs/plugin-rscalready setsemitAssets: trueon therscenvironment for the same reason — but does not extend that fix to thessrenvironment.Fix
Set
build.ssrEmitAssets: trueat the top level of vinext's vite config. This propagates intoenvironments.ssr.build.emitAssetsfor both the App Router SSR environment and the Pages Router SSR environment.Why the top-level form
build.ssrEmitAssetsis the documented Vite API for this exact case. Settingenvironments.ssr.build.emitAssetsdirectly does not work because Vite re-applies the top-level value duringresolveConfigand overwrites any per-environment value we set in theconfighook.The App Router path happens to escape this override because
@vitejs/plugin-rscsetsbuilder.sharedConfigBuild: true. The Pages Router path does not, so a per-env-only fix would silently regress the Pages Router case.Affected deploy fixtures
…and the broader
scss/**family that imports global CSS from server components or root layouts.Tests
tests/ssr-css-assets.test.ts:emitAssets: true.emitAssets: true.import "<spec>.css"statement indist/server/ssr/*.jsresolves to a file that exists on disk.All three tests fail on
mainand pass with this change.Related
Category A4 in the deploy-suite e2e review.
Generated as part of the deploy-suite fix sweep.