Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions packages/plugin-react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,20 @@ export default function viteReact(opts: Options = {}): Plugin[] {
},
}

const viteReactRefreshBundledDevModeResolveRuntime: Plugin = {
name: 'vite:react-refresh-fbm-resolve-runtime',
enforce: 'pre',
resolveId(id) {
if (skipFastRefresh || !isBundledDev || base === '/') return

const basePrefixedRuntimePublicPath =
base.slice(0, -1) + runtimePublicPath
if (id === basePrefixedRuntimePublicPath) {
return runtimePublicPath
}
},
}

const dependencies = [
'react',
'react-dom',
Expand Down Expand Up @@ -541,6 +555,7 @@ export default function viteReact(opts: Options = {}): Plugin[] {
...(isRolldownVite
? [viteRefreshWrapper, viteConfigPost, viteReactRefreshBundledDevMode]
: []),
viteReactRefreshBundledDevModeResolveRuntime,
viteReactRefresh,
Comment on lines 556 to 559
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

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

The resolve runtime plugin should be conditionally included only when isRolldownVite is true, similar to how other bundledDev-specific plugins are included on lines 555-557. While the plugin has internal guards that prevent it from running in non-rolldown contexts, it's more consistent and efficient to only include it when it can actually be used. Consider moving this line inside the conditional spread on line 556.

Suggested change
? [viteRefreshWrapper, viteConfigPost, viteReactRefreshBundledDevMode]
: []),
viteReactRefreshBundledDevModeResolveRuntime,
viteReactRefresh,
? [
viteRefreshWrapper,
viteConfigPost,
viteReactRefreshBundledDevMode,
viteReactRefreshBundledDevModeResolveRuntime,
]
: []),
viteReact,

Copilot uses AI. Check for mistakes.
virtualPreamblePlugin({
name: '@vitejs/plugin-react/preamble',
Expand Down
29 changes: 29 additions & 0 deletions packages/plugin-react/tests/rolldown.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import path from 'node:path'
import { runtimePublicPath } from '@vitejs/react-common'
import { type Plugin, rolldown } from 'rolldown'
import { expect, test } from 'vitest'
import pluginReact, { type Options } from '../src/index.ts'
Expand All @@ -21,6 +22,34 @@ test('HMR related code should not be included when using rolldown with babel plu
expect(output[0].code).not.toContain('import.meta.hot')
})

test('resolves base-prefixed refresh runtime id in bundledDev mode', () => {
const plugins = pluginReact()

const reactBabelPlugin = plugins.find(
(plugin) => plugin.name === 'vite:react-babel',
)
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

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

The test should include an assertion to verify that reactBabelPlugin is found before attempting to call its configResolved method. While optional chaining prevents runtime errors, an explicit assertion would make the test more robust and provide clearer feedback if the plugin structure changes. Add expect(reactBabelPlugin).toBeDefined() after line 30.

Suggested change
)
)
expect(reactBabelPlugin).toBeDefined()

Copilot uses AI. Check for mistakes.
reactBabelPlugin?.configResolved?.({
base: '/ui/',
command: 'serve',
isProduction: false,
root: '/',
server: { hmr: true },
plugins: [],
experimental: { bundledDev: true },
} as any)

const reactRefreshResolvePlugin = plugins.find(
(plugin) => plugin.name === 'vite:react-refresh-fbm-resolve-runtime',
)
expect(reactRefreshResolvePlugin?.resolveId).toBeDefined()

const resolved = (reactRefreshResolvePlugin?.resolveId as any)(
'/ui' + runtimePublicPath,
)
Comment on lines +46 to +48
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

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

The test hardcodes '/ui' instead of deriving it from the base value set on line 32. For better maintainability, consider constructing the test input consistently: either use '/ui/'.slice(0, -1) + runtimePublicPath or store the base in a variable and reuse it. This ensures the test remains correct if the base value is changed.

Copilot uses AI. Check for mistakes.

expect(resolved).toBe(runtimePublicPath)
})

async function bundleWithRolldown(pluginReactOptions: Options = {}) {
const ENTRY = '/entry.tsx'
const files: Record<string, string> = {
Expand Down
Loading