diff --git a/packages/plugin-react/src/index.ts b/packages/plugin-react/src/index.ts index 2cc9eaab5a5f75..6b7200a53d527b 100644 --- a/packages/plugin-react/src/index.ts +++ b/packages/plugin-react/src/index.ts @@ -2,7 +2,7 @@ import type { ParserOptions, TransformOptions, types as t } from '@babel/core' import * as babel from '@babel/core' import { createFilter } from '@rollup/pluginutils' import resolve from 'resolve' -import type { Plugin, PluginOption } from 'vite' +import type { Plugin, PluginOption, ResolvedConfig } from 'vite' import { addRefreshWrapper, isRefreshBoundary, @@ -43,6 +43,24 @@ export interface Options { parserPlugins?: ParserOptions['plugins'] } +type ReactBabelOptions = { + [P in keyof TransformOptions]-?: (P extends 'parserOpts' + ? { plugins: Exclude } + : unknown) & + Exclude +} + +declare module 'vite' { + export interface Plugin { + api?: { + /** + * Manipulate the Babel options of `@vitejs/plugin-react` + */ + reactBabel?: (options: ReactBabelOptions, config: ResolvedConfig) => void + } + } +} + export default function viteReact(opts: Options = {}): PluginOption[] { // Provide default values for Rollup compat. let base = '/' @@ -54,9 +72,16 @@ export default function viteReact(opts: Options = {}): PluginOption[] { const useAutomaticRuntime = opts.jsxRuntime !== 'classic' - const userPlugins = opts.babel?.plugins || [] - const userParserPlugins = - opts.parserPlugins || opts.babel?.parserOpts?.plugins || [] + const babelOptions = { + babelrc: false, + configFile: false, + ...opts.babel + } as ReactBabelOptions + + babelOptions.plugins ||= [] + babelOptions.presets ||= [] + babelOptions.parserOpts ||= {} as any + babelOptions.parserOpts.plugins ||= opts.parserPlugins || [] // Support pattens like: // - import * as React from 'react'; @@ -88,15 +113,21 @@ export default function viteReact(opts: Options = {}): PluginOption[] { ) } - config.plugins.forEach( - (plugin) => - (plugin.name === 'react-refresh' || - (plugin !== viteReactJsx && plugin.name === 'vite:react-jsx')) && - config.logger.warn( + config.plugins.forEach((plugin) => { + const hasConflict = + plugin.name === 'react-refresh' || + (plugin !== viteReactJsx && plugin.name === 'vite:react-jsx') + + if (hasConflict) + return config.logger.warn( `[@vitejs/plugin-react] You should stop using "${plugin.name}" ` + `since this plugin conflicts with it.` ) - ) + + if (plugin.api?.reactBabel) { + plugin.api.reactBabel(babelOptions, config) + } + }) }, async transform(code, id, options) { const ssr = typeof options === 'boolean' ? options : options?.ssr === true @@ -108,10 +139,10 @@ export default function viteReact(opts: Options = {}): PluginOption[] { [] if (/\.(mjs|[tj]sx?)$/.test(extension)) { - const plugins = [...userPlugins] + const plugins = [...babelOptions.plugins] - const parserPlugins: typeof userParserPlugins = [ - ...userParserPlugins, + const parserPlugins: typeof babelOptions.parserOpts.plugins = [ + ...babelOptions.parserOpts.plugins, 'importMeta', // This plugin is applied before esbuild transforms the code, // so we need to enable some stage 3 syntax that is supported in @@ -191,35 +222,32 @@ export default function viteReact(opts: Options = {}): PluginOption[] { } } - const isReasonReact = extension.endsWith('.bs.js') + const transformAsync = ast + ? babel.transformFromAstAsync.bind(babel, ast, code) + : babel.transformAsync.bind(babel, code) - const babelOpts: TransformOptions = { - babelrc: false, - configFile: false, - ...opts.babel, + const isReasonReact = extension.endsWith('.bs.js') + const result = await transformAsync({ + ...babelOptions, ast: !isReasonReact, root: projectRoot, filename: id, sourceFileName: id, parserOpts: { - ...opts.babel?.parserOpts, + ...babelOptions.parserOpts, sourceType: 'module', allowAwaitOutsideFunction: true, plugins: parserPlugins }, generatorOpts: { - ...opts.babel?.generatorOpts, + ...babelOptions.generatorOpts, decoratorsBeforeExport: true }, plugins, sourceMaps: true, // Vite handles sourcemap flattening inputSourceMap: false as any - } - - const result = ast - ? await babel.transformFromAstAsync(ast, code, babelOpts) - : await babel.transformAsync(code, babelOpts) + }) if (result) { let code = result.code!