diff --git a/packages/plugin-react/src/index.ts b/packages/plugin-react/src/index.ts index b41efc78a05255..dc7be2580f2753 100644 --- a/packages/plugin-react/src/index.ts +++ b/packages/plugin-react/src/index.ts @@ -36,13 +36,46 @@ export interface Options { /** * Babel configuration applied in both dev and prod. */ - babel?: TransformOptions + babel?: BabelOptions /** * @deprecated Use `babel.parserOpts.plugins` instead */ parserPlugins?: ParserOptions['plugins'] } +export type BabelOptions = Omit< + TransformOptions, + | 'ast' + | 'filename' + | 'root' + | 'sourceFileName' + | 'sourceMaps' + | 'inputSourceMap' +> + +/** + * The object type used by the `options` passed to plugins with + * an `api.reactBabel` method. + */ +export interface ReactBabelOptions extends BabelOptions { + plugins: Extract + presets: Extract + parserOpts: ParserOptions & { + plugins: Extract + } +} + +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,11 +87,18 @@ 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 - // Support pattens like: + babelOptions.plugins ||= [] + babelOptions.presets ||= [] + babelOptions.parserOpts ||= {} as any + babelOptions.parserOpts.plugins ||= opts.parserPlugins || [] + + // Support patterns like: // - import * as React from 'react'; // - import React from 'react'; // - import React, {useEffect} from 'react'; @@ -88,15 +128,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 @@ -113,7 +159,7 @@ export default function viteReact(opts: Options = {}): PluginOption[] { const isProjectFile = !isNodeModules && (id[0] === '\0' || id.startsWith(projectRoot + '/')) - const plugins = isProjectFile ? [...userPlugins] : [] + const plugins = isProjectFile ? [...babelOptions.plugins] : [] let useFastRefresh = false if (!skipFastRefresh && !ssr && !isNodeModules) { @@ -179,15 +225,15 @@ export default function viteReact(opts: Options = {}): PluginOption[] { // module, including node_modules and linked packages. const shouldSkip = !plugins.length && - !opts.babel?.configFile && - !(isProjectFile && opts.babel?.babelrc) + !babelOptions.configFile && + !(isProjectFile && babelOptions.babelrc) if (shouldSkip) { return // Avoid parsing if no plugins exist. } - 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 @@ -206,35 +252,32 @@ export default function viteReact(opts: Options = {}): PluginOption[] { parserPlugins.push('typescript') } - 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: filepath, 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!