Description
When experimental.bundledDev is enabled in a multi-environment Vite setup (e.g., client + Cloudflare Workers via @cloudflare/vite-plugin), environments that extend DevEnvironment (rather than FullBundleDevEnvironment) crash because vite:import-analysis is excluded from the shared plugin list.
The Environment API is designed to support heterogeneous environments in a single Vite process, but bundledDev is a global top-level toggle that strips plugins from the shared pipeline, affecting all environments equally — including those that still rely on the standard unbundled transform pipeline.
Reproduction
- Create a Vite project with multiple environments — a client environment and a Cloudflare Worker environment (using
@cloudflare/vite-plugin)
- Enable
experimental: { bundledDev: true } in vite.config.ts
- Run
vite dev
The worker environment crashes with:
Cannot find module 'virtual:cloudflare/nodejs-global-inject/@cloudflare/unenv-preset/node/process'
Root Cause Trace
1. importAnalysisPlugin excluded globally
In resolvePlugins(), the isBundled flag gates several plugins out of the shared list:
// vite/src/node/plugins/index.ts (resolvePlugins)
...isBundled ? [] : [
clientInjectionsPlugin(config),
cssAnalysisPlugin(config),
importAnalysisPlugin(config) // ← DROPPED when bundledDev is true
]
Where isBundled = config.experimental?.bundledDev || isBuild.
This exclusion is correct for FullBundleDevEnvironment (Rolldown handles its own import resolution), but incorrect for non-bundled environments that still use the standard DevEnvironment transform pipeline in the same Vite process.
2. Virtual module imports are not rewritten
Without importAnalysisPlugin, bare virtual module specifiers (e.g., virtual:cloudflare/...) are never rewritten to their URL form (/@id/__x00__virtual:cloudflare/...).
Normally, importAnalysisPlugin's transform resolves these via resolveId → \0virtual:cloudflare/..., then normalizeResolvedIdToUrl calls wrapId → /@id/__x00__virtual:cloudflare/.... This URL starts with /, which is critical for the next step.
3. fetchModule misroutes bare specifiers
In fetchModule(), the import specifier is checked:
// fetchModule
if (id.startsWith("file://") || id[0] === "." || id[0] === "/") {
// → goes through transformRequest (correct path)
} else {
// → enters tryNodeResolve (wrong path for virtual modules)
}
Without the /@id/ rewrite, virtual:cloudflare/... starts with v, enters the tryNodeResolve branch, and fails because it's not a real Node module.
4. The crash
tryNodeResolve throws: Cannot find module 'virtual:cloudflare/nodejs-global-inject/...'
Why it works without bundledDev
When bundledDev is false, importAnalysisPlugin is included in the shared plugin list. It rewrites virtual:cloudflare/... → /@id/__x00__virtual:cloudflare/... during transform. The /@id/ URL starts with /, bypasses tryNodeResolve in fetchModule, and correctly falls through to transformRequest which goes through the full plugin pipeline.
Impact
This blocks adoption of bundledDev for any project that uses Vite's Environment API with mixed environment types — the primary use case the Environment API was designed for. Concretely, this affects:
- Cloudflare Workers via
@cloudflare/vite-plugin (client + worker in one Vite process)
- Any custom environment that extends
DevEnvironment rather than FullBundleDevEnvironment
- Any environment plugin that registers virtual modules resolved through the standard transform pipeline
The Cloudflare Vite plugin is one of the most prominent consumers of the Environment API. This incompatibility means bundledDev is effectively unusable for a significant portion of the Environment API's target audience.
Suggested Fix
The cleanest resolution would be one of:
-
Make bundledDev per-environment — move it into DevEnvironmentOptions so only environments that opt in use FullBundleDevEnvironment, while others keep the standard pipeline with importAnalysisPlugin. This aligns with the Environment API's design intent of heterogeneous environments.
-
Keep importAnalysisPlugin in the shared list — have it no-op when running inside FullBundleDevEnvironment but still execute for standard DevEnvironment instances. Plugins already receive environment context during transform.
-
Conditionally include/exclude plugins per environment — let the shared plugin pipeline filter based on which environment is invoking it.
Option 1 seems most aligned with the Environment API architecture.
Environment
- Vite: 8.0.2
@cloudflare/vite-plugin: 1.30.0
- Node.js: 24.x
Description
When
experimental.bundledDevis enabled in a multi-environment Vite setup (e.g., client + Cloudflare Workers via@cloudflare/vite-plugin), environments that extendDevEnvironment(rather thanFullBundleDevEnvironment) crash becausevite:import-analysisis excluded from the shared plugin list.The Environment API is designed to support heterogeneous environments in a single Vite process, but
bundledDevis a global top-level toggle that strips plugins from the shared pipeline, affecting all environments equally — including those that still rely on the standard unbundled transform pipeline.Reproduction
@cloudflare/vite-plugin)experimental: { bundledDev: true }invite.config.tsvite devThe worker environment crashes with:
Root Cause Trace
1.
importAnalysisPluginexcluded globallyIn
resolvePlugins(), theisBundledflag gates several plugins out of the shared list:Where
isBundled = config.experimental?.bundledDev || isBuild.This exclusion is correct for
FullBundleDevEnvironment(Rolldown handles its own import resolution), but incorrect for non-bundled environments that still use the standardDevEnvironmenttransform pipeline in the same Vite process.2. Virtual module imports are not rewritten
Without
importAnalysisPlugin, bare virtual module specifiers (e.g.,virtual:cloudflare/...) are never rewritten to their URL form (/@id/__x00__virtual:cloudflare/...).Normally,
importAnalysisPlugin's transform resolves these viaresolveId→\0virtual:cloudflare/..., thennormalizeResolvedIdToUrlcallswrapId→/@id/__x00__virtual:cloudflare/.... This URL starts with/, which is critical for the next step.3.
fetchModulemisroutes bare specifiersIn
fetchModule(), the import specifier is checked:Without the
/@id/rewrite,virtual:cloudflare/...starts withv, enters thetryNodeResolvebranch, and fails because it's not a real Node module.4. The crash
tryNodeResolvethrows:Cannot find module 'virtual:cloudflare/nodejs-global-inject/...'Why it works without
bundledDevWhen
bundledDevis false,importAnalysisPluginis included in the shared plugin list. It rewritesvirtual:cloudflare/...→/@id/__x00__virtual:cloudflare/...during transform. The/@id/URL starts with/, bypassestryNodeResolveinfetchModule, and correctly falls through totransformRequestwhich goes through the full plugin pipeline.Impact
This blocks adoption of
bundledDevfor any project that uses Vite's Environment API with mixed environment types — the primary use case the Environment API was designed for. Concretely, this affects:@cloudflare/vite-plugin(client + worker in one Vite process)DevEnvironmentrather thanFullBundleDevEnvironmentThe Cloudflare Vite plugin is one of the most prominent consumers of the Environment API. This incompatibility means
bundledDevis effectively unusable for a significant portion of the Environment API's target audience.Suggested Fix
The cleanest resolution would be one of:
Make
bundledDevper-environment — move it intoDevEnvironmentOptionsso only environments that opt in useFullBundleDevEnvironment, while others keep the standard pipeline withimportAnalysisPlugin. This aligns with the Environment API's design intent of heterogeneous environments.Keep
importAnalysisPluginin the shared list — have it no-op when running insideFullBundleDevEnvironmentbut still execute for standardDevEnvironmentinstances. Plugins already receive environment context during transform.Conditionally include/exclude plugins per environment — let the shared plugin pipeline filter based on which environment is invoking it.
Option 1 seems most aligned with the Environment API architecture.
Environment
@cloudflare/vite-plugin: 1.30.0