fix(build): preserve server export conditions - #2918
Conversation
commit: |
|
|
/bigbonk review for issues |
Performance benchmarksCompared 1 improved · 0 regressed · 5 within ±1.5%
View detailed results and traces 🟢 improvement · 🔴 regression · ⚫ change below 1.5% · paired base/head |
There was a problem hiding this comment.
Review: fix(build): preserve server export conditions (#2918)
I verified the core mechanism end-to-end and it is correct. Summary below, plus a few non-blocking notes.
Verified correct
-
The bug is real.
@cloudflare/vite-plugin@1.31.0setsdefaultConditions = ["workerd", "worker", "module", "browser"]and applies it to the Worker environments via itsconfighook:resolve.conditions = [...defaultConditions, "development|production"](index.mjs:25438)optimizeDeps.rolldownOptions.resolve.conditionNames = [...defaultConditions, "development"](index.mjs:25480)
Both include
browser, so a vinext RSC/SSR graph could select a package'sbrowserexport during SSR — diverging from Next.js server resolution. The filter removes exactlybrowserand leavesworkerd/worker/module/development(|production)intact, so resolution falls through toimport. Correct. -
Environment targeting is right. The Cloudflare worker environment in vinext apps is named
rscwithssras a child (seeexamples/app-router-cloudflare/vite.config.ts:viteEnvironment: { name: "rsc", childEnvironments: ["ssr"] }). The Cloudflare plugin applies conditions to exactly the environments inenvironmentNameToWorkerMap, which are these same names. So thename !== "rsc" && name !== "ssr"guard matches precisely the environments that receivedbrowser. -
Hook ordering is right. The Cloudflare plugin injects conditions through its
confighook (environments: getEnvironmentsConfig(...), index.mjs:25619), notconfigEnvironment. Vite mergesconfig-hook output before running per-environmentconfigEnvironment, so by the time thisenforce: "post"configEnvironmentruns,config.resolve.conditionsalready contains the mergedbrowser. The filter sees and removes it. -
Mutation is safe. Mutating
configin place inconfigEnvironmentis an established pattern here (see thehasCloudflarePluginblock at index.ts:3411). The PR reassigns new filtered arrays (not in-place array mutation), so no shared/frozen array is mutated. The non-null assertions onconfig.optimizeDeps!.rolldownOptions!.resolve!.conditionNamesare guarded by the preceding optional-chainedoptimizerConditions?.includes("browser"), so they can't throw. -
No
applyguard is correct. SSR resolution divergence and dep-optimizer resolution both matter in dev, so running inserve+buildis intended. No downstream code readsresolve.conditions, so removingbrowserhas no other side effects.vp checkpasses on the changed file.
Non-blocking notes
-
Missing unit test in this extraction. The PR states behavioral tests "intentionally remain in #2877," but this repo already has a well-established pattern for unit-testing
configEnvironmenthooks in isolation without a real runtime — seetests/compiler-define.test.tsandtests/client-global-define.test.ts, which locate the plugin by name and callplugin.configEnvironment!("rsc", config, { command })against a plain config object. A focused test here would assert:rsc/ssrwithconditions: [..., "browser"]→browserremoved, all others preserved- same for
optimizeDeps.rolldownOptions.resolve.conditionNames clientand a no-browserconfig → untouched (no-op)
This is cheap, requires no workerd, and guards against a future adapter/Vite change silently reintroducing
browser. Since the fix is trivial to break silently, I'd recommend adding it in this PR rather than deferring to #2877. -
Name/comment now slightly misleading. Commit 2 removed the
hasCloudflarePlugingate so the filter applies to any adapter that injectsbrowser(per the PR's "regardless of deployment adapter" intent). But the plugin is still namedvinext:cloudflare-server-conditionsand the comment frames it around the RSC/SSR contract generically. Consider renaming to something adapter-neutral (e.g.vinext:server-export-conditions) to match the generalized behavior. Cosmetic. -
Rolldown-only optimizer branch. The optimizer filter only handles
optimizeDeps.rolldownOptions.resolve.conditionNames, not the esbuild branch (optimizeDeps.esbuildOptions.conditions) that the Cloudflare plugin still emits when!isRolldown. That's correct for this repo (Vite 8 bundles Rolldown,isRolldownis always true here), but it's an implicit coupling to the Rolldown path. A one-line comment noting the esbuild branch is intentionally not handled would help future readers.
Overall: the fix is correct and safe. My only real recommendation is adding the focused configEnvironment unit test given the established pattern makes it near-free and it's the exact kind of regression that would slip through silently.
|
Review posted to PR #2918. SummaryI verified the fix end-to-end against the actual Verified correct:
Non-blocking notes I raised:
No blocking issues found. |
|
/bigbonk review for issues |
There was a problem hiding this comment.
Review: fix(build): preserve server export conditions (#2918)
I re-verified this independently against the vendored @cloudflare/vite-plugin@1.31.0 source, the vinext plugin structure, and the existing configEnvironment mutation pattern. The fix is correct and safe. I concur with the prior review; adding a few points it didn't cover.
Verified correct
-
The bug is real and the source matches.
dist/index.mjsdefinesdefaultConditions = ["workerd", "worker", "module", "browser"](25410) and applies it to the worker environments increateCloudflareEnvironmentOptions:resolve.conditions = [...defaultConditions, "development|production"](25438)optimizeDeps.rolldownOptions.resolve.conditionNames = [...defaultConditions, "development"](25480)
Both include
browser, so a vinext RSC/SSR graph could pick a package'sbrowserexport during SSR. The filter removes exactlybrowserand leavesworkerd/worker/module/development(|production)intact, so resolution falls through toimport. Correct. -
Hook ordering is right. The Cloudflare plugin injects conditions through its
confighook (environments: getEnvironmentsConfig(...), 25619), notconfigEnvironment. Vite mergesconfig-hook output into each per-environment config before runningconfigEnvironment, so thisenforce: "post"configEnvironmentobserves the mergedbrowserand removes it. -
In-place mutation +
return nullis the established, working pattern here — not just a stylistic match. The existingconfigEnvironmentatindex.ts:3449-3451mutatesconfig.resolve.extensionsin place and returnsnull; Vite passes the actual per-environment config object by reference, so the mutation propagates. The new plugin follows this exactly and reassigns fresh filtered arrays (no in-place array mutation of a possibly-shared array), so it can't corrupt state shared across environments. Non-null assertions are guarded by the precedingoptimizerConditions?.includes("browser")check. -
Environment targeting is architecturally guaranteed, not incidental. vinext's own init/validation code hard-requires
name: "rsc"withchildEnvironments: ["ssr"]for the Cloudflare worker env (init-cloudflare.ts:1023,1053,1079;utils/project.ts:288). So thename !== "rsc" && name !== "ssr"guard matches precisely the environments an adapter would have populated withbrowser. -
The fix is consistent with vinext's existing design intent. vinext already documents that
browserexport conditions are wrong for server code — see the client-optimizerexcludecomment atindex.ts:3265, which describes the exact failure mode (file-typeresolving tocore.jsinstead of thenode-exportindex.js). This PR generalizes that same protection to conditions injected by deployment adapters. No vinext server environment setsbrowserintentionally, so there's no false-positive strip. -
vp check packages/vinext/src/index.tspasses (format, lint, type). No test references the plugin name, so the rename suggestion below is non-breaking.
Non-blocking notes
-
Missing unit test in this extraction (strongest recommendation). The PR defers behavioral tests to #2877, but the repo already has a near-free pattern for unit-testing
configEnvironmentin isolation — seetests/compiler-define.test.ts:159-161(plugin.configEnvironment!("rsc", config, { command })against a plain object, no runtime). A focused test would assert:rsc/ssrwithconditions: [..., "browser"]→browserremoved and all others preserved (same foroptimizeDeps.rolldownOptions.resolve.conditionNames);clientand a no-browserconfig → untouched. This exact class of regression (a future adapter/Vite bump silently reintroducingbrowser) would otherwise slip through completely silently. I'd add it here rather than deferring. -
Plugin name is now misleading. Commit 2 (
filter browser from all server graphs) dropped thehasCloudflarePlugingate so the filter is adapter-neutral, but the plugin is stillvinext:cloudflare-server-conditions. Rename to something likevinext:server-export-conditions. Cosmetic; no tests depend on the name. -
Rolldown-only optimizer branch is an implicit coupling. The optimizer filter handles only
optimizeDeps.rolldownOptions.resolve.conditionNames, not the esbuild branch (optimizeDeps.esbuildOptions.conditions) that the Cloudflare plugin still emits when!isRolldown(25491-25497). That's correct for this Vite 8 repo (isRolldownis always true), but a one-line comment noting the esbuild branch is intentionally unhandled would help future readers — and would pair well with the AGENTS.md guidance to treat esbuild config as a migration target, not a pattern to copy forward.
Verdict
Correct, safe, and consistent with vinext's existing server-resolution design. No blocking issues. The single recommendation I'd act on before merge is adding the focused configEnvironment unit test, since the pattern makes it trivial and it guards a silent-regression path.
|
Review posted to PR #2918. SummaryI independently verified the fix end-to-end against the vendored Verified correct:
Non-blocking notes (I concur with the prior review and added depth):
|
|
/bigbonk review for issues |
There was a problem hiding this comment.
Review: fix(build): preserve server export conditions (#2918)
I verified the fix against the vendored @cloudflare/vite-plugin@1.31.0 source, the vinext plugin structure, and the existing configEnvironment patterns. The fix is correct and safe — no blocking issues.
Verified correct
-
The bug is real.
dist/index.mjsdefinesdefaultConditions = ["workerd", "worker", "module", "browser"](25410) and applies it to bothresolve.conditions(25438) andoptimizeDeps.rolldownOptions.resolve.conditionNames(25480) on the worker environments. Both includebrowser, so a vinext RSC/SSR graph could select a package'sbrowserexport during SSR. The filter removes exactlybrowserand leaves everything else intact; resolution falls through toimport, thendefault. Removingbrowsercan never leave zero matching conditions (Node/Vite always fall back todefault), so it can't cause a resolution failure. -
Hook ordering is right. The Cloudflare plugin injects conditions via its
confighook (environments: getEnvironmentsConfig(...), 25619), notconfigEnvironment. Vite mergesconfig-hook output into each per-environment config before runningconfigEnvironment, so thisenforce: "post"hook observes the mergedbrowserand removes it. -
In-place mutation +
return nullis the established, working pattern. The existingconfigEnvironmentatindex.ts:3449-3451mutatesconfig.resolve.extensionsin place and returnsnull. Vite passes the per-environment config by reference, so the mutation propagates. The new hook follows this exactly and reassigns fresh filtered arrays (no in-place array mutation of a shared array). The non-null assertions at 4146 are guarded by the precedingoptimizerConditions?.includes("browser")check, so they can't throw. -
vp check/tsc --noEmitpass on the changed file.
Notes from prior reviews that are now stale
- The "plugin name is misleading (
vinext:cloudflare-server-conditions)" note from the two earlier reviews is already resolved. Commitef43f42renamed it tovinext:server-conditions(adapter-neutral). The current code (index.ts:4128) is correct.
Non-blocking notes
-
Missing unit test (recommended before merge). The PR defers behavioral tests to #2877, but the repo has a near-free pattern for testing
configEnvironmentin isolation — seetests/compiler-define.test.ts:159(plugin.configEnvironment!("rsc", config, { command })against a plain object, no runtime). A focused test would assert:rsc/ssrwithconditions: [..., "browser"]→browserremoved, all others preserved (same foroptimizeDeps.rolldownOptions.resolve.conditionNames);clientand a no-browserconfig → untouched. This is the exact class of regression (a future adapter/Vite bump silently reintroducingbrowser) that would otherwise slip through completely silently. -
Scoping to
rsc/ssrenv names depends on the vinext init contract — worth a comment. The filter only matches environments literally namedrsc/ssr. For App Router this is architecturally guaranteed:init-cloudflare.ts:1023-1090hard-validatesviteEnvironment: { name: "rsc", childEnvironments: ["ssr"] }. But whencloudflare()is used withoutviteEnvironment.name, the worker environment name defaults toworkerNameToEnvironmentName(topLevelName)(index.mjs:18405 — the wrangler worker name with dashes→underscores), notrsc/ssr.examples/pages-router-cloudflare/vite.config.tsuses a barecloudflare(), so its worker env would be namedpages_router_cloudflareand thebrowserfilter would not apply to it. This isn't a regression introduced by this PR (the example is just out of sync with whatvinext initscaffolds, which does addviteEnvironment: { name: "rsc" }for Pages Router too —init-cloudflare.ts:534-539), and the fix is correct for any project initialized the vinext-recommended way. But a one-line comment noting that this filter assumes the vinext env-naming contract (rsc/ssr) would help future readers, and it may be worth aligning the Pages Router example config or a follow-up to broaden the guard. -
Rolldown-only optimizer branch. The optimizer filter handles only
optimizeDeps.rolldownOptions.resolve.conditionNames, not the esbuild branch (optimizeDeps.esbuildOptions.conditions) the Cloudflare plugin emits when!isRolldown(25491-25497). Correct for this Vite 8 repo (isRolldownis always true), but a one-line comment noting the esbuild branch is intentionally unhandled would help — and pairs with the AGENTS.md guidance to treat esbuild config as a migration target, not a pattern to copy forward.
Verdict
Correct, safe, and consistent with vinext's existing server-resolution design (cf. the client-optimizer exclude comment at index.ts:3262-3269 documenting the same browser-export failure mode). No blocking issues. The one thing I'd act on before merge is the focused configEnvironment unit test — the pattern makes it trivial and it guards a silent-regression path.
|
Review posted to PR #2918. SummaryI independently verified this PR against the vendored Verified correct:
Notably, I caught a stale note from the two prior reviews: they flagged the plugin name New finding not in prior reviews: The filter scopes to environments literally named Non-blocking (concur with prior reviews):
|
Summary
browserexport condition from every vinextrscandssrenvironment, regardless of deployment adapterThis is the narrowly scoped production-code extraction of
vinext:server-conditionsfrom #2877. Environment identity is the contract: vinextrscandssrgraphs render server code, so filtering does not depend on detecting the Cloudflare plugin.Why
A deployment adapter can add
browserto its default resolution conditions, but a vinextrscorssrgraph must not select a package browser export. Doing so diverges from Next.js server resolution and can execute a client-only implementation during SSR. The filter removes onlybrowser; conditions for the active host remain available before resolution falls through toimport.Tests
The behavioral ESM-externals and real workerd tests intentionally remain in #2877, as requested. This extraction does not duplicate or move them.
Validation on this branch:
vp run vinext#buildvp run checkgit diff --checkRelated: #2877