Repository navigation
fix(rsc): reject client-only only when it survives RSC tree-shaking - #4011
james-elicx wants to merge 1 commit into
Conversation
@vitejs/plugin-rsc's rsc:validate-imports checks the client-only import chain in buildEnd, before tree-shaking, so a Server Component importing one export from a sideEffects:false barrel failed the build when an unused leaf imported client-only (e.g. @heroui/react -> react-aria Calendar). Claim client-only in the RSC build and fail only when the marker module is still in the emitted RSC chunks, matching Turbopack, which reports the client-only import error only for modules kept in the tree-shaken graph. Fixes #4007
|
/bigbonk review for issues |
@vinext/cloudflare
create-vinext-app
@vinext/types
vinext
@cloudflare/workers-response-store
commit: |
|
LGTM! |
|
Performance benchmarksCompared 0 improved · 0 regressed · 7 within ±1.5%
View detailed results and traces 🟢 improvement · 🔴 regression · ⚫ change below 1.5% · paired base/head |
|
Closing in favour of the upstream fix in @vitejs/plugin-rsc: vitejs/vite-plugin-react#1511 That PR moves |
Fixes #4007
Problem
A Server Component that imports one export from a
sideEffects: falsebarrel (e.g.import { Toast } from "@heroui/react") failedvite build:The page never uses
Calendar; the barrel just reaches it.Root cause
@vitejs/plugin-rsc'srsc:validate-importschecks theclient-onlyimport chain inbuildEnd, which runs before tree-shaking. So anyclient-onlyimport reachable through re-exports fails the build, even when the bundler drops the module and its marker from the output.What Next.js does
I checked the reporter's repro against real
next@16.2.7:next build(default)/with 200next dev/with 200next build --webpack'client-only' cannot be imported from a Server Component moduleTurbopack maps
client-onlyin the server graph to an import error, and reports it only for modules that stay in the tree-shaken graph (crates/next-core/src/next_import_map.rs,insert_client_only_error_alias). This PR follows the default (Turbopack) behavior.Fix
New build-only plugin,
vinext:rsc-client-only-imports:client-onlyin therscenvironment to a marker module with side effects.generateBundle, fails the build only if that marker is still in the emitted RSC chunks.rsc({ validateImports: false })removesrsc:validate-imports.Real misuse still fails the build:
client-only;sideEffects: false, whose module is kept for its side effects;require("client-only");import("client-only");Dev is unchanged: Vite dev doesn't tree-shake, so plugin-rsc's dev check still applies.
Tests
tests/client-only-tree-shaking.test.tsbuilds a tmp App Router fixture with local packages that copy both shapes from the issue:client-only(theCalendarshape);client-onlyand only re-exports a binding, inside a package withsideEffects: ["*.css"](the react-ariaToast.mjsshape).The positive case builds, starts the prod server and asserts that both components render. Five negative cases assert that the build still fails, each with the expected import chain.
tests/fixtures/cf-app-basicgets a checked-insideEffects: falsebarrel package and a/client-only-barrelpage.tests/e2e/cloudflare-encoded-paths/client-only-barrel.spec.tschecks that page on the Workers build.Validation
vp test run tests/client-only-tree-shaking.test.ts: 6/6 pass. Without the fix, the positive case fails with plugin-rsc's error.vp test run tests/middleware-server-only.test.ts: passes.cf-app-basic:vp buildfails withrsc:validate-importsonclient-only-barrel/widget.js;cloudflare-encoded-pathsproject, wrangler dev).@heroui/react3.2.1) fails without the fix; with the fix it builds and the prod server returns 200 with the page content.vp checkpasses on the changed files.Known limitation (unchanged by this PR)
vinext devon the reporter's repro still returns 500, with a different error:createContext is not a function. Vite dev evaluates the whole@heroui/reactbarrel under react-server React. That is unrelated to theclient-onlycheck, andexperimental.optimizePackageImportsis the existing lever for barrel evaluation in dev.