diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c395dddec..5ef2c8337 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/packages/plugin-react-oxc/CHANGELOG.md b/packages/plugin-react-oxc/CHANGELOG.md index 2e08df63d..4b94ef788 100644 --- a/packages/plugin-react-oxc/CHANGELOG.md +++ b/packages/plugin-react-oxc/CHANGELOG.md @@ -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. diff --git a/packages/plugin-react-oxc/src/index.ts b/packages/plugin-react-oxc/src/index.ts index 3b6318b02..3f35996f9 100644 --- a/packages/plugin-react-oxc/src/index.ts +++ b/packages/plugin-react-oxc/src/index.ts @@ -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( diff --git a/packages/plugin-react-swc/src/index.ts b/packages/plugin-react-swc/src/index.ts index 432f8fbeb..b781b2e36 100644 --- a/packages/plugin-react-swc/src/index.ts +++ b/packages/plugin-react-swc/src/index.ts @@ -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 } @@ -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', ) } }, diff --git a/packages/plugin-react/CHANGELOG.md b/packages/plugin-react/CHANGELOG.md index 0aabccc3e..d004d75d3 100644 --- a/packages/plugin-react/CHANGELOG.md +++ b/packages/plugin-react/CHANGELOG.md @@ -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. diff --git a/packages/plugin-react/src/index.ts b/packages/plugin-react/src/index.ts index 436091402..648da2cf8 100644 --- a/packages/plugin-react/src/index.ts +++ b/packages/plugin-react/src/index.ts @@ -8,6 +8,7 @@ import * as vite from 'vite' import type { Plugin, ResolvedConfig } from 'vite' import { addRefreshWrapper, + avoidSourceMapOption, getPreambleCode, preambleCode, runtimePublicPath, @@ -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< @@ -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() @@ -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, }, - optimizeDeps: - 'rolldownVersion' in vite - ? { rollupOptions: { jsx: { mode: 'automatic' } } } - : { esbuildOptions: { jsx: 'automatic' } }, + optimizeDeps: { esbuildOptions: { jsx: 'automatic' } }, } } }, @@ -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)) @@ -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 }, @@ -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', @@ -384,7 +440,7 @@ export default function viteReact(opts: Options = {}): Plugin[] { }, } - return [viteBabel, viteReactRefresh] + return [viteBabel, viteRefreshWrapper, viteConfigPost, viteReactRefresh] } viteReact.preambleCode = preambleCode diff --git a/packages/plugin-rsc/e2e/starter.test.ts b/packages/plugin-rsc/e2e/starter.test.ts index a66db75cf..2d49059a3 100644 --- a/packages/plugin-rsc/e2e/starter.test.ts +++ b/packages/plugin-rsc/e2e/starter.test.ts @@ -2,6 +2,7 @@ import { expect, test } from '@playwright/test' import { useFixture } from './fixture' import { defineStarterTest } from './starter' import { waitForHydration } from './helper' +import * as vite from 'vite' test.describe('dev-default', () => { const f = useFixture({ root: 'examples/starter', mode: 'dev' }) @@ -24,6 +25,8 @@ test.describe('build-cloudflare', () => { }) test.describe('dev-production', () => { + test.skip('rolldownVersion' in vite) + const f = useFixture({ root: 'examples/starter', mode: 'dev', @@ -42,6 +45,8 @@ test.describe('dev-production', () => { }) test.describe('build-development', () => { + test.skip('rolldownVersion' in vite) + const f = useFixture({ root: 'examples/starter', mode: 'build', diff --git a/playground/class-components/__tests__/oxc/class-components.spec.ts b/playground/class-components/__tests__/oxc/class-components.spec.ts deleted file mode 100644 index 471b6e892..000000000 --- a/playground/class-components/__tests__/oxc/class-components.spec.ts +++ /dev/null @@ -1 +0,0 @@ -import '../class-components.spec' diff --git a/playground/hmr-false/__tests__/oxc/hmr-false.spec.ts b/playground/hmr-false/__tests__/oxc/hmr-false.spec.ts deleted file mode 100644 index 57dff5bd9..000000000 --- a/playground/hmr-false/__tests__/oxc/hmr-false.spec.ts +++ /dev/null @@ -1 +0,0 @@ -import '../hmr-false.spec' diff --git a/playground/hook-with-jsx/__tests__/oxc/hook-with-jsx.spec.ts b/playground/hook-with-jsx/__tests__/oxc/hook-with-jsx.spec.ts deleted file mode 100644 index 4696fae50..000000000 --- a/playground/hook-with-jsx/__tests__/oxc/hook-with-jsx.spec.ts +++ /dev/null @@ -1 +0,0 @@ -import '../hook-with-jsx.spec' diff --git a/playground/mdx/__tests__/oxc/mdx.spec.ts b/playground/mdx/__tests__/oxc/mdx.spec.ts deleted file mode 100644 index 4f2df7980..000000000 --- a/playground/mdx/__tests__/oxc/mdx.spec.ts +++ /dev/null @@ -1 +0,0 @@ -import '../mdx.spec' diff --git a/playground/react-env/__tests__/oxc/react.spec.ts b/playground/react-env/__tests__/oxc/react.spec.ts deleted file mode 100644 index 776f43259..000000000 --- a/playground/react-env/__tests__/oxc/react.spec.ts +++ /dev/null @@ -1 +0,0 @@ -import '../react.spec' diff --git a/playground/react-sourcemap/__tests__/oxc/react-sourcemap.spec.ts b/playground/react-sourcemap/__tests__/oxc/react-sourcemap.spec.ts deleted file mode 100644 index 4fb7caa7e..000000000 --- a/playground/react-sourcemap/__tests__/oxc/react-sourcemap.spec.ts +++ /dev/null @@ -1 +0,0 @@ -import '../react-sourcemap.spec' diff --git a/playground/react/__tests__/oxc/react.spec.ts b/playground/react/__tests__/oxc/react.spec.ts deleted file mode 100644 index 776f43259..000000000 --- a/playground/react/__tests__/oxc/react.spec.ts +++ /dev/null @@ -1 +0,0 @@ -import '../react.spec' diff --git a/playground/ssr-react/__tests__/oxc/ssr-react.spec.ts b/playground/ssr-react/__tests__/oxc/ssr-react.spec.ts deleted file mode 100644 index 66fde5a3f..000000000 --- a/playground/ssr-react/__tests__/oxc/ssr-react.spec.ts +++ /dev/null @@ -1 +0,0 @@ -import '../ssr-react.spec' diff --git a/playground/vitest.config.e2e.ts b/playground/vitest.config.e2e.ts index 6a59bd577..412f3142f 100644 --- a/playground/vitest.config.e2e.ts +++ b/playground/vitest.config.e2e.ts @@ -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: { @@ -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, diff --git a/playground/vitestGlobalSetup.ts b/playground/vitestGlobalSetup.ts index a7fa1d6e7..508ec0f0a 100644 --- a/playground/vitestGlobalSetup.ts +++ b/playground/vitestGlobalSetup.ts @@ -40,55 +40,6 @@ export async function setup({ provide }: TestProject): Promise { 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 { diff --git a/playground/vitestSetup.ts b/playground/vitestSetup.ts index dda7a4ffb..0638b88a5 100644 --- a/playground/vitestSetup.ts +++ b/playground/vitestSetup.ts @@ -15,6 +15,14 @@ import type { import type { Browser, Page } from 'playwright-chromium' import type { RunnerTestFile } from 'vitest' import { beforeAll, inject } from 'vitest' +import { + build, + createBuilder, + createServer, + loadConfigFromFile, + mergeConfig, + preview, +} from 'vite' // #region env @@ -169,8 +177,6 @@ beforeAll(async (s) => { }) async function loadConfig(configEnv: ConfigEnv) { - const { loadConfigFromFile, mergeConfig } = await importVite() - let config: UserConfig | null = null // config file named by convention as the *.spec.ts folder @@ -220,9 +226,6 @@ async function loadConfig(configEnv: ConfigEnv) { } export async function startDefaultServe(): Promise { - const { build, createBuilder, createServer, mergeConfig, preview } = - await importVite() - setupConsoleWarnCollector(serverLogs) if (!isBuild) { @@ -353,11 +356,6 @@ function stripTrailingSlashIfNeeded(url: string, base: string): string { return url } -async function importVite(): Promise { - const vitePath = path.resolve(testDir, '_vite-proxy.js') - return await import(vitePath) -} - declare module 'vite' { export interface UserConfig { /**