diff --git a/packages/plugin-react-oxc/CHANGELOG.md b/packages/plugin-react-oxc/CHANGELOG.md index 1fc7ebfe0..7648dec4d 100644 --- a/packages/plugin-react-oxc/CHANGELOG.md +++ b/packages/plugin-react-oxc/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Add `filter` for rolldown-vite + +Added `filter` so that it is more performant when running this plugin with rolldown-powered version of Vite. + ## 0.1.1 (2025-04-10) ## 0.1.0 (2025-04-09) diff --git a/packages/plugin-react-oxc/package.json b/packages/plugin-react-oxc/package.json index f31de7df1..8b9321249 100644 --- a/packages/plugin-react-oxc/package.json +++ b/packages/plugin-react-oxc/package.json @@ -46,5 +46,8 @@ "@vitejs/react-common": "workspace:*", "unbuild": "^3.5.0", "vite": "catalog:rolldown-vite" + }, + "dependencies": { + "@rolldown/pluginutils": "1.0.0-beta.9" } } diff --git a/packages/plugin-react-oxc/src/index.ts b/packages/plugin-react-oxc/src/index.ts index 377d4c042..f234ad80d 100644 --- a/packages/plugin-react-oxc/src/index.ts +++ b/packages/plugin-react-oxc/src/index.ts @@ -9,6 +9,7 @@ import { runtimePublicPath, silenceUseClientWarning, } from '@vitejs/react-common' +import { exactRegex } from '@rolldown/pluginutils' const _dirname = dirname(fileURLToPath(import.meta.url)) @@ -149,12 +150,3 @@ export default function viteReact(opts: Options = {}): PluginOption[] { return [viteConfig, viteRefreshRuntime, viteRefreshWrapper] } - -function exactRegex(input: string): RegExp { - return new RegExp(`^${escapeRegex(input)}$`) -} - -const escapeRegexRE = /[-/\\^$*+?.()|[\]{}]/g -function escapeRegex(str: string): string { - return str.replace(escapeRegexRE, '\\$&') -} diff --git a/packages/plugin-react-swc/CHANGELOG.md b/packages/plugin-react-swc/CHANGELOG.md index be8be4c6f..79ed6f409 100644 --- a/packages/plugin-react-swc/CHANGELOG.md +++ b/packages/plugin-react-swc/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Add `filter` for rolldown-vite + +Added `filter` so that it is more performant when running this plugin with rolldown-powered version of Vite. + ### Skip HMR preamble in Vitest browser mode This was causing annoying `Sourcemap for "/@react-refresh" points to missing source files` and is unnecessary in test mode. diff --git a/packages/plugin-react-swc/package.json b/packages/plugin-react-swc/package.json index dd1140604..e41f5cb0a 100644 --- a/packages/plugin-react-swc/package.json +++ b/packages/plugin-react-swc/package.json @@ -29,6 +29,7 @@ }, "homepage": "https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-react-swc#readme", "dependencies": { + "@rolldown/pluginutils": "1.0.0-beta.9", "@swc/core": "^1.11.22" }, "peerDependencies": { diff --git a/packages/plugin-react-swc/src/index.ts b/packages/plugin-react-swc/src/index.ts index 0185ece14..6f20a05df 100644 --- a/packages/plugin-react-swc/src/index.ts +++ b/packages/plugin-react-swc/src/index.ts @@ -18,6 +18,7 @@ import { runtimePublicPath, silenceUseClientWarning, } from '@vitejs/react-common' +import { exactRegex } from '@rolldown/pluginutils' /* eslint-disable no-restricted-globals */ const _dirname = @@ -96,14 +97,23 @@ const react = (_options?: Options): PluginOption[] => { name: 'vite:react-swc:resolve-runtime', apply: 'serve', enforce: 'pre', // Run before Vite default resolve to avoid syscalls - resolveId: (id) => (id === runtimePublicPath ? id : undefined), - load: (id) => - id === runtimePublicPath - ? readFileSync(join(_dirname, 'refresh-runtime.js'), 'utf-8').replace( - /__README_URL__/g, - 'https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-react-swc', - ) - : undefined, + resolveId: { + filter: { id: exactRegex(runtimePublicPath) }, + handler: (id) => (id === runtimePublicPath ? id : undefined), + }, + load: { + filter: { id: exactRegex(runtimePublicPath) }, + handler: (id) => + id === runtimePublicPath + ? readFileSync( + join(_dirname, 'refresh-runtime.js'), + 'utf-8', + ).replace( + /__README_URL__/g, + 'https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-react-swc', + ) + : undefined, + }, }, { name: 'vite:react-swc', diff --git a/packages/plugin-react/CHANGELOG.md b/packages/plugin-react/CHANGELOG.md index 86c6efd09..ccb6b02c9 100644 --- a/packages/plugin-react/CHANGELOG.md +++ b/packages/plugin-react/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Add `filter` for rolldown-vite + +Added `filter` so that it is more performant when running this plugin with rolldown-powered version of Vite. + ## 4.4.1 (2025-04-19) Fix type issue when using `moduleResolution: "node"` in tsconfig [#462](https://github.com/vitejs/vite-plugin-react/pull/462) diff --git a/packages/plugin-react/package.json b/packages/plugin-react/package.json index e893b948b..cc6bb6ddc 100644 --- a/packages/plugin-react/package.json +++ b/packages/plugin-react/package.json @@ -51,6 +51,7 @@ "@babel/core": "^7.26.10", "@babel/plugin-transform-react-jsx-self": "^7.25.9", "@babel/plugin-transform-react-jsx-source": "^7.25.9", + "@rolldown/pluginutils": "1.0.0-beta.9", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, diff --git a/packages/plugin-react/src/index.ts b/packages/plugin-react/src/index.ts index 66d280f93..250c4477e 100644 --- a/packages/plugin-react/src/index.ts +++ b/packages/plugin-react/src/index.ts @@ -13,6 +13,10 @@ import { runtimePublicPath, silenceUseClientWarning, } from '@vitejs/react-common' +import { + exactRegex, + makeIdFiltersToMatchWithQuery, +} from '@rolldown/pluginutils' const _dirname = dirname(fileURLToPath(import.meta.url)) @@ -102,7 +106,10 @@ const defaultIncludeRE = /\.[tj]sx?$/ const tsRE = /\.tsx?$/ export default function viteReact(opts: Options = {}): PluginOption[] { - const filter = createFilter(opts.include ?? defaultIncludeRE, opts.exclude) + const include = opts.include ?? defaultIncludeRE + const exclude = opts.exclude + const filter = createFilter(include, exclude) + const jsxImportSource = opts.jsxImportSource ?? 'react' const jsxImportRuntime = `${jsxImportSource}/jsx-runtime` const jsxImportDevRuntime = `${jsxImportSource}/jsx-dev-runtime` @@ -181,114 +188,130 @@ export default function viteReact(opts: Options = {}): PluginOption[] { // we only create static option in this case and re-create them // each time otherwise staticBabelOptions = createBabelOptions(opts.babel) + + if ( + canSkipBabel(staticBabelOptions.plugins, staticBabelOptions) && + skipFastRefresh && + (opts.jsxRuntime === 'classic' ? isProduction : true) + ) { + delete viteBabel.transform + } } }, - async transform(code, id, options) { - if (id.includes('/node_modules/')) return - - const [filepath] = id.split('?') - if (!filter(filepath)) return - - const ssr = options?.ssr === true - const babelOptions = (() => { - if (staticBabelOptions) return staticBabelOptions - const newBabelOptions = createBabelOptions( - typeof opts.babel === 'function' - ? opts.babel(id, { ssr }) - : opts.babel, - ) - runPluginOverrides?.(newBabelOptions, { id, ssr }) - return newBabelOptions - })() - const plugins = [...babelOptions.plugins] - - const isJSX = filepath.endsWith('x') - const useFastRefresh = - !skipFastRefresh && - !ssr && - (isJSX || - (opts.jsxRuntime === 'classic' - ? importReactRE.test(code) - : code.includes(jsxImportDevRuntime) || - code.includes(jsxImportRuntime))) - if (useFastRefresh) { - plugins.push([ - await loadPlugin('react-refresh/babel'), - { skipEnvCheck: true }, - ]) - } - - if (opts.jsxRuntime === 'classic' && isJSX) { - if (!isProduction) { - // These development plugins are only needed for the classic runtime. - plugins.push( - await loadPlugin('@babel/plugin-transform-react-jsx-self'), - await loadPlugin('@babel/plugin-transform-react-jsx-source'), + transform: { + filter: { + id: { + include: makeIdFiltersToMatchWithQuery(include), + exclude: [ + ...(exclude + ? makeIdFiltersToMatchWithQuery(ensureArray(exclude)) + : []), + /\/node_modules\//, + ], + }, + }, + async handler(code, id, options) { + if (id.includes('/node_modules/')) return + + const [filepath] = id.split('?') + if (!filter(filepath)) return + + const ssr = options?.ssr === true + const babelOptions = (() => { + if (staticBabelOptions) return staticBabelOptions + const newBabelOptions = createBabelOptions( + typeof opts.babel === 'function' + ? opts.babel(id, { ssr }) + : opts.babel, ) + runPluginOverrides?.(newBabelOptions, { id, ssr }) + return newBabelOptions + })() + const plugins = [...babelOptions.plugins] + + const isJSX = filepath.endsWith('x') + const useFastRefresh = + !skipFastRefresh && + !ssr && + (isJSX || + (opts.jsxRuntime === 'classic' + ? importReactRE.test(code) + : code.includes(jsxImportDevRuntime) || + code.includes(jsxImportRuntime))) + if (useFastRefresh) { + plugins.push([ + await loadPlugin('react-refresh/babel'), + { skipEnvCheck: true }, + ]) } - } - // Avoid parsing if no special transformation is needed - if ( - !plugins.length && - !babelOptions.presets.length && - !babelOptions.configFile && - !babelOptions.babelrc - ) { - return - } + if (opts.jsxRuntime === 'classic' && isJSX) { + if (!isProduction) { + // These development plugins are only needed for the classic runtime. + plugins.push( + await loadPlugin('@babel/plugin-transform-react-jsx-self'), + await loadPlugin('@babel/plugin-transform-react-jsx-source'), + ) + } + } - const parserPlugins = [...babelOptions.parserOpts.plugins] + // Avoid parsing if no special transformation is needed + if (canSkipBabel(plugins, babelOptions)) { + return + } - if (!filepath.endsWith('.ts')) { - parserPlugins.push('jsx') - } + const parserPlugins = [...babelOptions.parserOpts.plugins] - if (tsRE.test(filepath)) { - parserPlugins.push('typescript') - } + if (!filepath.endsWith('.ts')) { + parserPlugins.push('jsx') + } - const babel = await loadBabel() - const result = await babel.transformAsync(code, { - ...babelOptions, - root: projectRoot, - filename: id, - sourceFileName: filepath, - // Required for esbuild.jsxDev to provide correct line numbers - // This creates issues the react compiler because the re-order is too important - // People should use @babel/plugin-transform-react-jsx-development to get back good line numbers - retainLines: - getReactCompilerPlugin(plugins) != null - ? false - : !isProduction && isJSX && opts.jsxRuntime !== 'classic', - parserOpts: { - ...babelOptions.parserOpts, - sourceType: 'module', - allowAwaitOutsideFunction: true, - plugins: parserPlugins, - }, - generatorOpts: { - ...babelOptions.generatorOpts, - // import attributes parsing available without plugin since 7.26 - importAttributesKeyword: 'with', - decoratorsBeforeExport: true, - }, - plugins, - sourceMaps: true, - }) + if (tsRE.test(filepath)) { + parserPlugins.push('typescript') + } - if (result) { - if (!useFastRefresh) { - return { code: result.code!, map: result.map } + const babel = await loadBabel() + const result = await babel.transformAsync(code, { + ...babelOptions, + root: projectRoot, + filename: id, + sourceFileName: filepath, + // Required for esbuild.jsxDev to provide correct line numbers + // This creates issues the react compiler because the re-order is too important + // People should use @babel/plugin-transform-react-jsx-development to get back good line numbers + retainLines: + getReactCompilerPlugin(plugins) != null + ? false + : !isProduction && isJSX && opts.jsxRuntime !== 'classic', + parserOpts: { + ...babelOptions.parserOpts, + sourceType: 'module', + allowAwaitOutsideFunction: true, + plugins: parserPlugins, + }, + generatorOpts: { + ...babelOptions.generatorOpts, + // import attributes parsing available without plugin since 7.26 + importAttributesKeyword: 'with', + decoratorsBeforeExport: true, + }, + plugins, + sourceMaps: true, + }) + + if (result) { + if (!useFastRefresh) { + return { code: result.code!, map: result.map } + } + return addRefreshWrapper( + result.code!, + result.map!, + '@vitejs/plugin-react', + id, + opts.reactRefreshHost, + ) } - return addRefreshWrapper( - result.code!, - result.map!, - '@vitejs/plugin-react', - id, - opts.reactRefreshHost, - ) - } + }, }, } @@ -319,18 +342,24 @@ export default function viteReact(opts: Options = {}): PluginOption[] { dedupe: ['react', 'react-dom'], }, }), - resolveId(id) { - if (id === runtimePublicPath) { - return id - } + resolveId: { + filter: { id: exactRegex(runtimePublicPath) }, + handler(id) { + if (id === runtimePublicPath) { + return id + } + }, }, - load(id) { - if (id === runtimePublicPath) { - return readFileSync(refreshRuntimePath, 'utf-8').replace( - /__README_URL__/g, - 'https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-react', - ) - } + load: { + filter: { id: exactRegex(runtimePublicPath) }, + handler(id) { + if (id === runtimePublicPath) { + return readFileSync(refreshRuntimePath, 'utf-8').replace( + /__README_URL__/g, + 'https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-react', + ) + } + }, }, transformIndexHtml(_, config) { if (!skipFastRefresh) @@ -349,6 +378,18 @@ export default function viteReact(opts: Options = {}): PluginOption[] { viteReact.preambleCode = preambleCode +function canSkipBabel( + plugins: ReactBabelOptions['plugins'], + babelOptions: ReactBabelOptions, +) { + return !( + plugins.length || + babelOptions.presets.length || + babelOptions.configFile || + babelOptions.babelrc + ) +} + const loadedPlugin = new Map() function loadPlugin(path: string): any { const cached = loadedPlugin.get(path) @@ -408,3 +449,7 @@ function getReactCompilerRuntimeModule( } return moduleName } + +function ensureArray(value: T | T[]): T[] { + return Array.isArray(value) ? value : [value] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5e97246fa..bb0724961 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -94,6 +94,9 @@ importers: '@babel/plugin-transform-react-jsx-source': specifier: ^7.25.9 version: 7.25.9(@babel/core@7.26.10) + '@rolldown/pluginutils': + specifier: 1.0.0-beta.9 + version: 1.0.0-beta.9 '@types/babel__core': specifier: ^7.20.5 version: 7.20.5 @@ -109,6 +112,10 @@ importers: version: 3.5.0(typescript@5.8.3) packages/plugin-react-oxc: + dependencies: + '@rolldown/pluginutils': + specifier: 1.0.0-beta.9 + version: 1.0.0-beta.9 devDependencies: '@vitejs/react-common': specifier: workspace:* @@ -122,6 +129,9 @@ importers: packages/plugin-react-swc: dependencies: + '@rolldown/pluginutils': + specifier: 1.0.0-beta.9 + version: 1.0.0-beta.9 '@swc/core': specifier: ^1.11.22 version: 1.11.22 @@ -1490,6 +1500,9 @@ packages: cpu: [x64] os: [win32] + '@rolldown/pluginutils@1.0.0-beta.9': + resolution: {integrity: sha512-e9MeMtVWo186sgvFFJOPGy7/d2j2mZhLJIdVW0C/xDluuOvymEATqz6zKsP0ZmXGzQtqlyjz5sC1sYQUoJG98w==} + '@rollup/plugin-alias@5.1.1': resolution: {integrity: sha512-PR9zDb+rOzkRb2VD+EuKB7UC41vU5DIwZ5qqCpk0KJudcWAyi8rvYOhS7+L5aZCspw1stTViLgN5v6FF1p5cgQ==} engines: {node: '>=14.0.0'} @@ -1716,6 +1729,7 @@ packages: '@swc/core@1.11.22': resolution: {integrity: sha512-mjPYbqq8XjwqSE0hEPT9CzaJDyxql97LgK4iyvYlwVSQhdN1uK0DBG4eP9PxYzCS2MUGAXB34WFLegdUj5HGpg==} engines: {node: '>=10'} + deprecated: It has a bug. See https://github.com/swc-project/swc/issues/10413 peerDependencies: '@swc/helpers': '>=0.5.17' peerDependenciesMeta: @@ -4767,6 +4781,8 @@ snapshots: '@rolldown/binding-win32-x64-msvc@1.0.0-beta.8-commit.2686eb1': optional: true + '@rolldown/pluginutils@1.0.0-beta.9': {} + '@rollup/plugin-alias@5.1.1(rollup@4.37.0)': optionalDependencies: rollup: 4.37.0