Skip to content

fix: four Rolldown rc.9 compatibility patches#21860

Open
shlomimatichin wants to merge 4 commits intovitejs:mainfrom
valencesec:fix/rolldown-rc9-compat-patches-upstream
Open

fix: four Rolldown rc.9 compatibility patches#21860
shlomimatichin wants to merge 4 commits intovitejs:mainfrom
valencesec:fix/rolldown-rc9-compat-patches-upstream

Conversation

@shlomimatichin
Copy link

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::Napi not yet implementing warn() in Rolldown rc.9, or from ordering/initialization issues in the new bundled builtin plugin system.

Commits

1. fix(worker): replace this.warn() with console.warn() in renderChunk

File: packages/vite/src/node/plugins/worker.ts

In the vite:worker plugin's renderChunk hook, 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 by PluginContext::Napi, which has warn() unimplemented in Rust — causing an immediate panic:

thread 'tokio-runtime-worker' panicked at 'not yet implemented: PluginContext::Napi::warn'

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.ts

In bundled mode, the native builtin:vite-module-preload-polyfill plugin loses a resolveId race against builtin:vite-resolve in Rolldown rc.9. The native resolver runs first and fails because vite/modulepreload-polyfill is not in Vite 8's package.json exports map, producing:

"./modulepreload-polyfill" is not exported from package "vite"

The fix uses the JS plugin path (with a resolveId hook) even in bundled mode. The JS hook is placed before builtin:vite-resolve in the plugin array and correctly intercepts the virtual module ID before the native resolver runs.

3. fix(resolve): provide onWarn callback in all modes

File: packages/vite/src/node/plugins/resolve.ts

viteResolvePlugin only provided an onWarn callback to builtin:vite-resolve in serve mode (command === 'serve'). In build mode, resolution warnings had no JS callback to dispatch to, so Rolldown fell through to the native PluginContext::Napi::warn() — which panics in rc.9.

The fix always provides onWarn regardless of command mode. The only behavioral difference is the clear option (clearing the terminal per-warning makes sense in serve mode but not in build mode).

4. fix(importAnalysisBuild): null-safe removedPureCssFilesCache lookup

File: packages/vite/src/node/plugins/importAnalysisBuild.ts

In generateBundle, the code accessed removedPureCssFilesCache.get(config)! with a non-null assertion. In bundled multi-environment builds, the cache is populated in renderChunk only when pure-CSS chunks are actually encountered. If a particular environment (e.g., a minimal SPA entry with no pure-CSS chunks) never triggers renderChunk, the cache entry is undefined, causing:

TypeError: Cannot read properties of undefined (reading 'get')

Replaced with optional chaining: removedPureCssFilesCache.get(config)?.get(filename). When undefined, the existing if (chunk) guard below correctly skips the pure-CSS rewriting logic.

Test plan

Verified against a production multi-page Vite 8 project with:

  • 5 SPA entry points (spa, signinapp, feedback, backoffice, authlanderresult)
  • vite build completes successfully for all pages (~106s total, down from ~283s on Vite 5)
  • vite dev server starts and serves the app without errors
  • Monaco editor, Ant Design, and babel-runtime dependencies all function correctly

shlomimatichin and others added 4 commits March 14, 2026 20:52
…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.
Copy link
Member

@sapphi-red sapphi-red left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need a minimal reproduction or a test that fails without this change and passes with this change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants