Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(plugin-react): allow options.babel to be a function #6238

Merged
merged 4 commits into from
May 20, 2022
Merged
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 67 additions & 22 deletions packages/plugin-react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ export interface Options {
/**
* Babel configuration applied in both dev and prod.
*/
babel?: BabelOptions
babel?:
| BabelOptions
| ((id: string, options: { ssr?: boolean }) => BabelOptions)
cyco130 marked this conversation as resolved.
Show resolved Hide resolved
}

export type BabelOptions = Omit<
Expand All @@ -59,6 +61,8 @@ export type BabelOptions = Omit<
* an `api.reactBabel` method.
*/
export interface ReactBabelOptions extends BabelOptions {
ssr?: boolean
file: string
cyco130 marked this conversation as resolved.
Show resolved Hide resolved
plugins: Extract<BabelOptions['plugins'], any[]>
presets: Extract<BabelOptions['presets'], any[]>
overrides: Extract<BabelOptions['overrides'], any[]>
Expand All @@ -67,13 +71,18 @@ export interface ReactBabelOptions extends BabelOptions {
}
}

type ReactBabelHook = (
options: ReactBabelOptions,
config: ResolvedConfig
) => void

declare module 'vite' {
export interface Plugin {
api?: {
/**
* Manipulate the Babel options of `@vitejs/plugin-react`
*/
reactBabel?: (options: ReactBabelOptions, config: ResolvedConfig) => void
reactBabel?: ReactBabelHook
}
}
}
Expand All @@ -86,21 +95,11 @@ export default function viteReact(opts: Options = {}): PluginOption[] {
let projectRoot = process.cwd()
let skipFastRefresh = opts.fastRefresh === false
let skipReactImport = false
let runPluginOverrides = (options: ReactBabelOptions) => false
let staticBabelOptions: ReactBabelOptions | undefined

const useAutomaticRuntime = opts.jsxRuntime !== 'classic'

const babelOptions = {
babelrc: false,
configFile: false,
...opts.babel
} as ReactBabelOptions

babelOptions.plugins ||= []
babelOptions.presets ||= []
babelOptions.overrides ||= []
babelOptions.parserOpts ||= {} as any
babelOptions.parserOpts.plugins ||= []

// Support patterns like:
// - import * as React from 'react';
// - import React from 'react';
Expand Down Expand Up @@ -141,11 +140,22 @@ export default function viteReact(opts: Options = {}): PluginOption[] {
`[@vitejs/plugin-react] You should stop using "${plugin.name}" ` +
`since this plugin conflicts with it.`
)
})

runPluginOverrides = (babelOptions) => {
const hooks = config.plugins
.map((plugin) => plugin.api?.reactBabel)
.filter(Boolean) as ReactBabelHook[]
cyco130 marked this conversation as resolved.
Show resolved Hide resolved

if (plugin.api?.reactBabel) {
plugin.api.reactBabel(babelOptions, config)
if (hooks.length > 0) {
return (runPluginOverrides = (babelOptions) => {
hooks.forEach((hook) => hook(babelOptions, config))
return true
})(babelOptions)
}
})
runPluginOverrides = () => false
return false
}
},
async transform(code, id, options) {
const ssr = typeof options === 'boolean' ? options : options?.ssr === true
Expand All @@ -162,7 +172,19 @@ export default function viteReact(opts: Options = {}): PluginOption[] {
const isProjectFile =
!isNodeModules && (id[0] === '\0' || id.startsWith(projectRoot + '/'))

const plugins = isProjectFile ? [...babelOptions.plugins] : []
let reactBabelOptions = staticBabelOptions
if (typeof opts.babel === 'function') {
const rawOptions = opts.babel(id, { ssr })
reactBabelOptions = createBabelOptions(id, rawOptions, ssr)
runPluginOverrides(reactBabelOptions)
} else if (!reactBabelOptions) {
reactBabelOptions = createBabelOptions(id, opts.babel, ssr)
if (!runPluginOverrides(reactBabelOptions)) {
staticBabelOptions = reactBabelOptions
}
}

const plugins = isProjectFile ? [...reactBabelOptions.plugins] : []

let useFastRefresh = false
if (!skipFastRefresh && !ssr && !isNodeModules) {
Expand Down Expand Up @@ -229,15 +251,15 @@ export default function viteReact(opts: Options = {}): PluginOption[] {
// module, including node_modules and linked packages.
const shouldSkip =
!plugins.length &&
!babelOptions.configFile &&
!(isProjectFile && babelOptions.babelrc)
!reactBabelOptions.configFile &&
!(isProjectFile && reactBabelOptions.babelrc)

if (shouldSkip) {
return // Avoid parsing if no plugins exist.
}

const parserPlugins: typeof babelOptions.parserOpts.plugins = [
...babelOptions.parserOpts.plugins,
const parserPlugins: typeof reactBabelOptions.parserOpts.plugins = [
...reactBabelOptions.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
Expand All @@ -261,6 +283,7 @@ export default function viteReact(opts: Options = {}): PluginOption[] {
: babel.transformAsync.bind(babel, code)

const isReasonReact = extension.endsWith('.bs.js')
const { ssr: _ssr, file: _file, ...babelOptions } = reactBabelOptions
const result = await transformAsync({
...babelOptions,
ast: !isReasonReact,
Expand Down Expand Up @@ -368,3 +391,25 @@ viteReact.preambleCode = preambleCode
function loadPlugin(path: string): Promise<any> {
return import(path).then((module) => module.default || module)
}

function createBabelOptions(
file: string,
rawOptions?: BabelOptions,
ssr?: boolean
) {
const babelOptions = {
ssr,
file,
Copy link
Member

@aleclarson aleclarson May 20, 2022

Choose a reason for hiding this comment

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

Another option is to use Object.defineProperties to define ssr and file options with enumerable: false (note this is the default when using defineProperties). That will prevent Babel from getting them, since non-enumerable properties are ignored by the spread operator.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I feel the intention would be less obvious and require a comment. But I can do that if you feel it'd be better.

Copy link
Member

@patak-dev patak-dev May 20, 2022

Choose a reason for hiding this comment

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

Why are these in the same object? I think the API would be more clear with three args. ssr and file are quite different than the rest of the object

type ReactBabelHook = (
  babelConfig: ReactBabelOptions,
  options: { ssr, file }, // or context?
  config: ResolvedConfig
) => void

Copy link
Member

Choose a reason for hiding this comment

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

ok let's do that, and I vote context as the name

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

Copy link
Member

Choose a reason for hiding this comment

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

Thanks for the changes @cyco130!

Sorry, one last question 👀
Why is it called file? Now that it is in the second parameter, should we rename it to id? file isn't that good in case there are virtual modules, no?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed (wasn't my idea anyway :P )

babelrc: false,
configFile: false,
...rawOptions
} as ReactBabelOptions

babelOptions.plugins ||= []
babelOptions.presets ||= []
babelOptions.overrides ||= []
babelOptions.parserOpts ||= {} as any
babelOptions.parserOpts.plugins ||= []

return babelOptions
}