Skip to content
Merged
11 changes: 11 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,17 @@ jobs:
- name: Test SWC
run: pnpm --filter ./packages/plugin-react-swc run test

- name: Setup rolldown-vite
run: |
sed -i"" -e "s/overrides:/overrides:\n vite: catalog:rolldown-vite/" pnpm-workspace.yaml
pnpm i --no-frozen-lockfile

- name: Test serve (rolldown-vite)
run: pnpm run test-serve

- name: Test build (rolldown-vite)
run: pnpm run test-build

lint:
if: github.repository == 'vitejs/vite-plugin-react'
timeout-minutes: 10
Expand Down
4 changes: 4 additions & 0 deletions packages/plugin-react-oxc/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### Deprecate this plugin

The changes of this plugin is now included in `@vitejs/plugin-react`. Please use `@vitejs/plugin-react` instead.

### Allow processing files in `node_modules`

The default value of `exclude` options is now `[/\/node_modules\//]` to allow processing files in `node_modules` directory. It was previously `[]` and files in `node_modules` was always excluded regardless of the value of `exclude` option.
Expand Down
7 changes: 7 additions & 0 deletions packages/plugin-react-oxc/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,13 @@ export default function viteReact(opts: Options = {}): Plugin[] {
},
}
},
configResolved(config) {
config.logger.warn(
'@vitejs/plugin-react-oxc is deprecated. ' +
'Please use @vitejs/plugin-react instead. ' +
'The changes of this plugin is now included in @vitejs/plugin-react.',
)
},
options() {
if (!this.meta.rolldownVersion) {
throw new Error(
Expand Down
4 changes: 2 additions & 2 deletions packages/plugin-react-swc/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ type Options = {
useAtYourOwnRisk_mutateSwcOptions?: (options: SWCOptions) => void

/**
* If set, disables the recommendation to use `@vitejs/plugin-react-oxc`
* If set, disables the recommendation to use `@vitejs/plugin-react`
*/
disableOxcRecommendation?: boolean
}
Expand Down Expand Up @@ -158,7 +158,7 @@ const react = (_options?: Options): Plugin[] => {
!options.disableOxcRecommendation
) {
config.logger.warn(
'[vite:react-swc] We recommend switching to `@vitejs/plugin-react-oxc` for improved performance as no swc plugins are used. More information at https://vite.dev/rolldown',
'[vite:react-swc] We recommend switching to `@vitejs/plugin-react` for improved performance as no swc plugins are used. More information at https://vite.dev/rolldown',
)
}
},
Expand Down
6 changes: 6 additions & 0 deletions packages/plugin-react/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

## Unreleased

### Use Oxc for react refresh transform in rolldown-vite

When used with rolldown-vite, this plugin now uses Oxc for react refresh transform.

Since this behavior is what `@vitejs/plugin-react-oxc` did, `@vitejs/plugin-react-oxc` is now deprecated and the `disableOxcRecommendation` option is removed.

### Allow processing files in `node_modules`

The default value of `exclude` options is now `[/\/node_modules\//]` to allow processing files in `node_modules` directory. It was previously `[]` and files in `node_modules` was always excluded regardless of the value of `exclude` option.
Expand Down
112 changes: 84 additions & 28 deletions packages/plugin-react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import * as vite from 'vite'
import type { Plugin, ResolvedConfig } from 'vite'
import {
addRefreshWrapper,
avoidSourceMapOption,
getPreambleCode,
preambleCode,
runtimePublicPath,
Expand Down Expand Up @@ -58,11 +59,6 @@ export interface Options {
* reactRefreshHost: 'http://localhost:3000'
*/
reactRefreshHost?: string

/**
* If set, disables the recommendation to use `@vitejs/plugin-react-oxc`
*/
disableOxcRecommendation?: boolean
}

export type BabelOptions = Omit<
Expand Down Expand Up @@ -115,6 +111,8 @@ export default function viteReact(opts: Options = {}): Plugin[] {
const jsxImportSource = opts.jsxImportSource ?? 'react'
const jsxImportRuntime = `${jsxImportSource}/jsx-runtime`
const jsxImportDevRuntime = `${jsxImportSource}/jsx-dev-runtime`

const isRolldownVite = 'rolldownVersion' in vite
let runningInVite = false
let isProduction = true
let projectRoot = process.cwd()
Expand All @@ -133,37 +131,53 @@ export default function viteReact(opts: Options = {}): Plugin[] {
const viteBabel: Plugin = {
name: 'vite:react-babel',
enforce: 'pre',
config() {
if (opts.jsxRuntime === 'classic') {
if ('rolldownVersion' in vite) {
config(_userConfig, { command }) {
if ('rolldownVersion' in vite) {
if (opts.jsxRuntime === 'classic') {
return {
oxc: {
jsx: {
runtime: 'classic',
refresh: command === 'serve',
// disable __self and __source injection even in dev
// as this plugin injects them by babel and oxc will throw
// if development is enabled and those properties are already present
development: false,
},
jsxRefreshInclude: include,
jsxRefreshExclude: exclude,
},
}
} else {
return {
esbuild: {
jsx: 'transform',
oxc: {
jsx: {
runtime: 'automatic',
importSource: jsxImportSource,
refresh: command === 'serve',
development: command === 'serve',
},
jsxRefreshInclude: include,
jsxRefreshExclude: exclude,
},
optimizeDeps: { rollupOptions: { jsx: { mode: 'automatic' } } },
}
}
}

if (opts.jsxRuntime === 'classic') {
return {
esbuild: {
jsx: 'transform',
},
}
} else {
return {
esbuild: {
jsx: 'automatic',
jsxImportSource: opts.jsxImportSource,
jsxImportSource: jsxImportSource,
Copy link

Choose a reason for hiding this comment

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

https://esbuild.github.io/api/#jsx-import-source

Alternatively, if you are using TypeScript, you can just configure the JSX import source for TypeScript by adding this to your tsconfig.json file and esbuild should pick it up automatically without needing to be configured:

{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "preact"
  }
}

Hi @sapphi-red, this behavior doesn't work properly in v5, are we considering to bring back this "feature"? Thanks.

Copy link
Member Author

Choose a reason for hiding this comment

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

I can revert this change of this line. But does that setup work properly? I guess it would try to optimize react dependencies and would show a warning / error related to that.

Copy link

Choose a reason for hiding this comment

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

optimize react dependencies

Okay, got your concern, thanks.

},
optimizeDeps:
'rolldownVersion' in vite
? { rollupOptions: { jsx: { mode: 'automatic' } } }
: { esbuildOptions: { jsx: 'automatic' } },
optimizeDeps: { esbuildOptions: { jsx: 'automatic' } },
}
}
},
Expand All @@ -180,17 +194,6 @@ export default function viteReact(opts: Options = {}): Plugin[] {
.map((plugin) => plugin.api?.reactBabel)
.filter(defined)

if (
'rolldownVersion' in vite &&
!opts.babel &&
!hooks.length &&
!opts.disableOxcRecommendation
) {
config.logger.warn(
'[vite:react-babel] We recommend switching to `@vitejs/plugin-react-oxc` for improved performance. More information at https://vite.dev/rolldown',
)
}

if (hooks.length > 0) {
runPluginOverrides = (babelOptions, context) => {
hooks.forEach((hook) => hook(babelOptions, context, config))
Expand Down Expand Up @@ -252,7 +255,7 @@ export default function viteReact(opts: Options = {}): Plugin[] {
? importReactRE.test(code)
: code.includes(jsxImportDevRuntime) ||
code.includes(jsxImportRuntime)))
if (useFastRefresh) {
if (useFastRefresh && !isRolldownVite) {
plugins.push([
await loadPlugin('react-refresh/babel'),
{ skipEnvCheck: true },
Expand Down Expand Up @@ -329,6 +332,59 @@ export default function viteReact(opts: Options = {}): Plugin[] {
},
}

const viteRefreshWrapper: Plugin = {
name: 'vite:react:refresh-wrapper',
apply: 'serve',
transform: isRolldownVite
? {
filter: {
id: {
include: makeIdFiltersToMatchWithQuery(include),
exclude: makeIdFiltersToMatchWithQuery(exclude),
},
},
handler(code, id, options) {
const ssr = options?.ssr === true

const [filepath] = id.split('?')
const isJSX = filepath.endsWith('x')
const useFastRefresh =
!skipFastRefresh &&
!ssr &&
(isJSX ||
code.includes(jsxImportDevRuntime) ||
code.includes(jsxImportRuntime))
if (!useFastRefresh) return

const { code: newCode } = addRefreshWrapper(
code,
avoidSourceMapOption,
'@vitejs/plugin-react',
id,
)
return { code: newCode, map: null }
},
}
: undefined,
}

const viteConfigPost: Plugin = {
name: 'vite:react:config-post',
enforce: 'post',
config(userConfig) {
if (userConfig.server?.hmr === false) {
return {
oxc: {
jsx: {
refresh: false,
},
},
// oxc option is only available in rolldown-vite
} as any
}
},
}

const dependencies = [
'react',
'react-dom',
Expand Down Expand Up @@ -384,7 +440,7 @@ export default function viteReact(opts: Options = {}): Plugin[] {
},
}

return [viteBabel, viteReactRefresh]
return [viteBabel, viteRefreshWrapper, viteConfigPost, viteReactRefresh]
}

viteReact.preambleCode = preambleCode
Expand Down
5 changes: 5 additions & 0 deletions packages/plugin-rsc/e2e/starter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
} from './helper'
import path from 'node:path'
import fs from 'node:fs'
import * as vite from 'vite'

test.describe('dev-default', () => {
const f = useFixture({ root: 'examples/starter', mode: 'dev' })
Expand Down Expand Up @@ -44,6 +45,8 @@ test.describe('build-no-ssr', () => {
})

test.describe('dev-production', () => {
test.skip('rolldownVersion' in vite)

Comment on lines 27 to +29
Copy link
Contributor

Choose a reason for hiding this comment

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

I have test cases for NODE_ENV=production vite dev and NODE_ENV=development vite build #606, but these are failing on oxc since jsx runtime transform respects command instead of NODE_ENV.

For now I'll skip this case on rolldown since the behavior is same as previous plugin-react-oxc and also I'm not sure about this use case in the first place.

Copy link
Member Author

@sapphi-red sapphi-red Jul 24, 2025

Choose a reason for hiding this comment

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

I think it is a bug in plugin-react-oxc. It should skip setting the development option as then it would default to isProduction.
I'll fix this in a separate PR.

Copy link
Contributor

Choose a reason for hiding this comment

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

Created an issue #622

const f = useFixture({
root: 'examples/starter',
mode: 'dev',
Expand All @@ -62,6 +65,8 @@ test.describe('dev-production', () => {
})

test.describe('build-development', () => {
test.skip('rolldownVersion' in vite)

const f = useFixture({
root: 'examples/starter',
mode: 'build',
Expand Down

This file was deleted.

1 change: 0 additions & 1 deletion playground/hmr-false/__tests__/oxc/hmr-false.spec.ts

This file was deleted.

This file was deleted.

1 change: 0 additions & 1 deletion playground/mdx/__tests__/oxc/mdx.spec.ts

This file was deleted.

1 change: 0 additions & 1 deletion playground/react-env/__tests__/oxc/react.spec.ts

This file was deleted.

This file was deleted.

1 change: 0 additions & 1 deletion playground/react/__tests__/oxc/react.spec.ts

This file was deleted.

1 change: 0 additions & 1 deletion playground/ssr-react/__tests__/oxc/ssr-react.spec.ts

This file was deleted.

7 changes: 1 addition & 6 deletions playground/vitest.config.e2e.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { resolve } from 'node:path'
import { defaultExclude, defineConfig } from 'vitest/config'
import { defineConfig } from 'vitest/config'

const timeout = process.env.PWDEBUG ? Infinity : process.env.CI ? 20_000 : 5_000

const isBelowNode20 = +process.versions.node.split('.')[0] < 20

export default defineConfig({
resolve: {
alias: {
Expand All @@ -14,9 +12,6 @@ export default defineConfig({
test: {
pool: 'forks',
include: ['./playground/**/*.spec.[tj]s'],
exclude: isBelowNode20
? ['**/__tests__/oxc/**', ...defaultExclude] // plugin-oxc only supports node >= 20
: defaultExclude,
setupFiles: ['./playground/vitestSetup.ts'],
globalSetup: ['./playground/vitestGlobalSetup.ts'],
testTimeout: timeout,
Expand Down
49 changes: 0 additions & 49 deletions playground/vitestGlobalSetup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,55 +40,6 @@ export async function setup({ provide }: TestProject): Promise<void> {
throw error
}
})

const playgrounds = (
await fs.readdir(path.resolve(__dirname, '../playground'), {
withFileTypes: true,
})
).filter((dirent) => dirent.name !== 'node_modules' && dirent.isDirectory())
for (const { name: playgroundName } of playgrounds) {
// write vite proxy file to load vite from each playground
await fs.writeFile(
path.resolve(tempDir, `${playgroundName}/_vite-proxy.js`),
"export * from 'vite';",
)

// also setup dedicated copy for plugin-react-oxc tests
const oxcTestDir = path.resolve(
__dirname,
'../playground',
playgroundName,
'__tests__/oxc',
)
if (!(await fs.exists(oxcTestDir))) continue

const variantPlaygroundName = `${playgroundName}__oxc`
await fs.copy(
path.resolve(tempDir, playgroundName),
path.resolve(tempDir, variantPlaygroundName),
)
await fs.remove(
path.resolve(
tempDir,
`${variantPlaygroundName}/node_modules/@vitejs/plugin-react`,
),
)
await fs.symlink(
path.resolve(__dirname, '../packages/plugin-react-oxc'),
path.resolve(
tempDir,
`${variantPlaygroundName}/node_modules/@vitejs/plugin-react`,
),
)
await fs.symlink(
path.resolve(__dirname, '../packages/plugin-react-oxc/node_modules/vite'),
path.resolve(tempDir, `${variantPlaygroundName}/node_modules/vite`),
)
await fs.copy(
path.resolve(__dirname, '../packages/plugin-react-oxc/node_modules/.bin'),
path.resolve(tempDir, `${variantPlaygroundName}/node_modules/.bin`),
)
}
}

export async function teardown(): Promise<void> {
Expand Down
Loading
Loading