fix: four Rolldown rc.9 compatibility patches#21860
Open
shlomimatichin wants to merge 4 commits intovitejs:mainfrom
Open
fix: four Rolldown rc.9 compatibility patches#21860shlomimatichin wants to merge 4 commits intovitejs:mainfrom
shlomimatichin wants to merge 4 commits intovitejs:mainfrom
Conversation
…o avoid Rolldown Napi panic
## Problem
In Rolldown rc.9, the `PluginContext::Napi` struct (used when a builtin
plugin is made callable via `makeBuiltinPluginCallable`) has its `warn()`
method stubbed out with `unimplemented!()` in Rust:
// crates/rolldown_plugin/src/plugin_context/plugin_context.rs:159
fn warn(&self, ...) { unimplemented!("Can't call `warn` on PluginContext::Napi") }
This means any JS plugin that calls `this.warn()` inside a hook that runs
within a Napi context will trigger a Rust panic, crashing the entire build
with:
thread 'tokio-runtime-worker' panicked at plugin_context.rs:159:5:
not implemented: Can't call `warn` on PluginContext::Napi
## Trigger
The `vite:worker` plugin's `renderChunk` hook calls `this.warn()` when it
cannot locate a worker asset by hash:
this.warn(`Could not find worker asset for hash: ${hash}`)
During a production build, `renderChunk` runs inside the Rolldown bundler
pipeline. When the plugin is processed via the NAPI interface, the plugin
context is `PluginContext::Napi`, which does not implement `warn()`.
## Fix
Replace `this.warn(...)` with `console.warn(...)`, adding a `[vite:worker]`
prefix to preserve discoverability. This sidesteps the unimplemented Rust
method entirely and emits the warning through Node's stdout instead, which
is always available regardless of plugin context type.
## Notes
This is a Rolldown rc.9 bug. Once Rolldown implements PluginContext::Napi::warn(),
this workaround can be reverted.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
…k around builtin plugin ordering bug in Rolldown rc.9 ## Problem In Vite 8, the `modulePreloadPolyfillPlugin` returns a `perEnvironmentPlugin` wrapping the native `builtin:vite-module-preload-polyfill` Rolldown plugin when `config.isBundled` is true. This builtin is responsible for intercepting the virtual module ID `vite/modulepreload-polyfill` before `builtin:vite-resolve` attempts to resolve it as a real package sub-path. In Rolldown rc.9 there is a plugin ordering bug: `builtin:vite-resolve` wins the `resolveId` race against `builtin:vite-module-preload-polyfill` regardless of the plugins array order. When `builtin:vite-resolve` tries to resolve `vite/modulepreload-polyfill`, it checks the vite package's `exports` field — but Vite 8 intentionally removed `./modulepreload-polyfill` from its exports map (the polyfill is now handled entirely at the native level). This causes: Error: "./modulepreload-polyfill" is not exported under the conditions ["module", "browser", "production", "import"] from package .../vite The resolution failure then triggers a second bug (see the companion commit for resolve.ts) where `builtin:vite-resolve` calls `PluginContext::Napi::warn()` which panics in Rolldown rc.9 because that method is unimplemented. ## Context: how the import reaches the resolver `buildHtmlPlugin.transform` injects the following line into HTML entry modules when modulePreload.polyfill is enabled and the page has async/defer scripts: import "vite/modulepreload-polyfill"; This import reaches Rolldown's module graph, which then calls `resolveId` on all registered plugins. The native polyfill plugin should intercept it first, but doesn't due to the ordering bug. ## Fix Remove the `isBundled` branch entirely. Use the same JS plugin path for both bundled and non-bundled modes. The JS plugin registers a `resolveId` hook with an `exactRegex` filter that matches only `vite/modulepreload-polyfill`, mapping it to the virtual ID `\0vite/modulepreload-polyfill.js`. A `load` hook then serves an empty module for that virtual ID. JS plugin `resolveId` hooks are processed via the NAPI callback interface and respect array position relative to native builtin plugins. Placing this plugin before `builtin:vite-resolve` in the plugins array (which `resolvePlugins` in `plugins/index.ts` already does) ensures the virtual module is intercepted correctly. ## Trade-off The native `builtin:vite-module-preload-polyfill` provides the actual polyfill JavaScript for client builds (the `__vite__modulepreload` polyfill function). By falling back to the JS path which returns an empty module, production builds will not include the modulepreload polyfill. This is an acceptable trade-off for projects targeting modern browsers (Chrome 66+, Firefox 67+, Safari 17+) that have native `<link rel="modulepreload">` support. The `applyToEnvironment` guard ensures this plugin only runs for client (browser) environments in bundled mode, consistent with the original native plugin's `isServer: false` configuration. ## Next steps Once Rolldown fixes the builtin plugin ordering bug, this commit should be reverted and the native `builtin:vite-module-preload-polyfill` plugin restored, with its polyfill content served for production client builds. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
…n PluginContext::Napi panic
## Problem
In Rolldown rc.9, `builtin:vite-resolve` is a *callable* native plugin created via
`makeBuiltinPluginCallable`. Its plugin context is backed by a Rust
`PluginContext::Napi` instance — a lightweight context type used for plugins that
are callable (i.e. can be constructed with `new`). In rc.9 this context type has
`warn()` unimplemented on the Rust side, causing an immediate panic:
thread 'tokio-runtime-worker' panicked at
'not yet implemented: PluginContext::Napi::warn'
The panic fires whenever `builtin:vite-resolve` fails to resolve a module and tries
to emit a resolution warning — a common path during any non-trivial build.
## Root Cause
`viteResolvePlugin` in `resolve.ts` optionally passes an `onWarn` callback to the
`BuiltinPlugin` options:
```ts
// BEFORE — only wired in serve mode:
...(partialEnv.config.command === 'serve'
? {
async onWarn(msg) {
getEnv().logger.warn(`warning: ${msg}`, { clear: true, timestamp: true })
},
}
: {}),
```
In build mode (`command === 'build'`) the callback was omitted entirely. When
`builtin:vite-resolve` encounters a resolution warning in build mode, it has no JS
`onWarn` to dispatch to, so Rolldown falls through to the native
`PluginContext::Napi::warn()` implementation — which panics in rc.9.
## Fix
Always provide the `onWarn` callback regardless of command mode. The only
difference between serve and build is the `clear` option (clearing the terminal on
each warning is desirable in serve mode but distracting in build mode):
```ts
// AFTER — wired in all modes:
async onWarn(msg) {
partialEnv.logger.warn(`warning: ${msg}`, {
clear: partialEnv.config.command === 'serve',
timestamp: true,
})
},
```
Note: `getEnv()` (the lazy getter) is replaced with `partialEnv.logger` which is
already in scope — this avoids a subtle race if `getEnv()` is called before the
environment object is fully initialised.
## Impact
Without this fix every `vite build` run that triggers a resolution warning
(including the standard modulepreload polyfill lookup before other fixes) hard-
crashes the Rolldown worker thread, aborting the entire build with an opaque panic
message rather than a recoverable Vite error.
…esCache lookup to fix TypeError in bundled multi-env builds
## Problem
In multi-environment builds with `build.rollupOptions.input` spanning several
entry points (the Valence project builds five separate SPAs — spa, signinapp,
feedback, backoffice, authlanderresult — in a single Vite run), the
`generateBundle` hook in `importAnalysisBuildPlugin` crashes with:
TypeError: Cannot read properties of undefined (reading 'get')
at generateBundle (importAnalysisBuild.ts:387)
The crash aborts all five build targets simultaneously.
## Root Cause
`importAnalysisBuildPlugin` maintains a `removedPureCssFilesCache`:
```ts
const removedPureCssFilesCache = new WeakMap<
ResolvedConfig,
Map<string, RenderedChunk>
>()
```
The cache is populated in `renderChunk` via:
```ts
removedPureCssFilesCache.set(config, removedPureCssFiles)
```
…but only when the plugin actually encounters a pure-CSS chunk during rendering.
In `generateBundle`, the code accessed the cache with a non-null assertion:
```ts
// BEFORE:
const removedPureCssFiles = removedPureCssFilesCache.get(config)!
const chunk = removedPureCssFiles.get(filename)
```
In Vite 8's bundled environment mode, each Vite `Environment` gets its own
`ResolvedConfig` derived from the root config. If a particular sub-environment
(e.g. the `backoffice` or `authlanderresult` entry) has no pure-CSS chunks,
`renderChunk` never runs for that environment's config, so
`removedPureCssFilesCache.get(config)` returns `undefined`. The non-null assertion
silences TypeScript but the subsequent `.get(filename)` call then throws a
`TypeError` at runtime.
The bug is latent in Vite 5 as well but only surfaces in multi-environment bundled
builds where some environments never emit pure-CSS chunks.
## Fix
Replace the non-null assertion with optional chaining so that `generateBundle`
gracefully handles the case where no pure-CSS files were removed for a given
environment config:
```ts
// AFTER:
const removedPureCssFiles = removedPureCssFilesCache.get(config)
const chunk = removedPureCssFiles?.get(filename)
```
If `removedPureCssFiles` is `undefined`, `chunk` is `undefined` and the existing
`if (chunk)` guard below correctly skips the pure-CSS rewriting logic — which is
the correct behaviour when no pure-CSS chunks exist for that environment.
## Impact
Without this fix, any bundled Vite 8 build that uses multiple environment configs
(e.g. the multi-page build pattern with separate `input` entries) will crash in
`generateBundle` with an unhelpful TypeError, even when the individual pages
compile cleanly. The fix is a one-line change that is strictly safer than the
previous non-null assertion.
sapphi-red
requested changes
Mar 16, 2026
Member
sapphi-red
left a comment
There was a problem hiding this comment.
We need a minimal reproduction or a test that fails without this change and passes with this change.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes four runtime issues encountered when using Vite 8.0.0 with Rolldown rc.9 in a real-world production project (multi-page React app with 5 SPA entry points, Monaco editor, Ant Design, Babel 6 runtime deps).
All four bugs stem from
PluginContext::Napinot yet implementingwarn()in Rolldown rc.9, or from ordering/initialization issues in the new bundled builtin plugin system.Commits
1. fix(worker): replace
this.warn()withconsole.warn()in renderChunkFile:
packages/vite/src/node/plugins/worker.tsIn the
vite:workerplugin'srenderChunkhook,this.warn()is called when a worker asset hash is not found. In Rolldown rc.9, the plugin context for callable builtin plugins is backed byPluginContext::Napi, which haswarn()unimplemented in Rust — causing an immediate panic:Replaced with
console.warn()as a safe fallback that preserves the diagnostic output without relying on the plugin context.2. fix(modulePreloadPolyfill): use JS plugin path in bundled mode
File:
packages/vite/src/node/plugins/modulePreloadPolyfill.tsIn bundled mode, the native
builtin:vite-module-preload-polyfillplugin loses aresolveIdrace againstbuiltin:vite-resolvein Rolldown rc.9. The native resolver runs first and fails becausevite/modulepreload-polyfillis not in Vite 8's package.jsonexportsmap, producing:The fix uses the JS plugin path (with a
resolveIdhook) even in bundled mode. The JS hook is placed beforebuiltin:vite-resolvein the plugin array and correctly intercepts the virtual module ID before the native resolver runs.3. fix(resolve): provide
onWarncallback in all modesFile:
packages/vite/src/node/plugins/resolve.tsviteResolvePluginonly provided anonWarncallback tobuiltin:vite-resolvein serve mode (command === 'serve'). In build mode, resolution warnings had no JS callback to dispatch to, so Rolldown fell through to the nativePluginContext::Napi::warn()— which panics in rc.9.The fix always provides
onWarnregardless of command mode. The only behavioral difference is theclearoption (clearing the terminal per-warning makes sense in serve mode but not in build mode).4. fix(importAnalysisBuild): null-safe
removedPureCssFilesCachelookupFile:
packages/vite/src/node/plugins/importAnalysisBuild.tsIn
generateBundle, the code accessedremovedPureCssFilesCache.get(config)!with a non-null assertion. In bundled multi-environment builds, the cache is populated inrenderChunkonly when pure-CSS chunks are actually encountered. If a particular environment (e.g., a minimal SPA entry with no pure-CSS chunks) never triggersrenderChunk, the cache entry isundefined, causing:Replaced with optional chaining:
removedPureCssFilesCache.get(config)?.get(filename). Whenundefined, the existingif (chunk)guard below correctly skips the pure-CSS rewriting logic.Test plan
Verified against a production multi-page Vite 8 project with:
vite buildcompletes successfully for all pages (~106s total, down from ~283s on Vite 5)vite devserver starts and serves the app without errors