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 1 commit
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
69 changes: 40 additions & 29 deletions packages/plugin-react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export interface Options {
*/
babel?:
| BabelOptions
| ((id: string, options?: { ssr?: boolean }) => BabelOptions)
| ((id: string, options: { ssr?: boolean }) => BabelOptions)
cyco130 marked this conversation as resolved.
Show resolved Hide resolved
}

export type BabelOptions = Omit<
Expand All @@ -61,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 @@ -69,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 @@ -88,23 +95,17 @@ 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'

let babelOptions: ReactBabelOptions

if (typeof opts.babel !== 'function') {
babelOptions = createBabelOptions(opts.babel)
}

// Support patterns like:
// - import * as React from 'react';
// - import React from 'react';
// - import React, {useEffect} from 'react';
const importReactRE = /(^|\n)import\s+(\*\s+as\s+)?React(,|\s+)/

let resolvedConfig: ResolvedConfig

// Any extension, including compound ones like '.bs.js'
const fileExtensionRE = /\.[^\/\s\?]+$/

Expand Down Expand Up @@ -141,11 +142,20 @@ export default function viteReact(opts: Options = {}): PluginOption[] {
)
})

if (typeof opts.babel !== 'function') {
runPluginOverrides(config, babelOptions)
}
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

resolvedConfig = 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,10 +172,16 @@ export default function viteReact(opts: Options = {}): PluginOption[] {
const isProjectFile =
!isNodeModules && (id[0] === '\0' || id.startsWith(projectRoot + '/'))

let babelOptions = staticBabelOptions
if (typeof opts.babel === 'function') {
const resolvedOpts = opts.babel(id, options)
babelOptions = createBabelOptions(resolvedOpts)
runPluginOverrides(resolvedConfig, babelOptions)
const rawOptions = opts.babel(id, { ssr })
babelOptions = createBabelOptions(id, rawOptions, ssr)
runPluginOverrides(babelOptions)
} else if (!babelOptions) {
babelOptions = createBabelOptions(id, opts.babel, ssr)
if (!runPluginOverrides(babelOptions)) {
staticBabelOptions = babelOptions
cyco130 marked this conversation as resolved.
Show resolved Hide resolved
}
}

const plugins = isProjectFile ? [...babelOptions.plugins] : []
Expand Down Expand Up @@ -375,8 +391,14 @@ function loadPlugin(path: string): Promise<any> {
return import(path).then((module) => module.default || module)
}

function createBabelOptions(rawOptions?: BabelOptions) {
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
Expand All @@ -390,14 +412,3 @@ function createBabelOptions(rawOptions?: BabelOptions) {

return babelOptions
}

function runPluginOverrides(
config: ResolvedConfig,
babelOptions: ReactBabelOptions
) {
config.plugins.forEach((plugin) => {
if (plugin.api?.reactBabel) {
plugin.api.reactBabel(babelOptions, config)
}
})
}