Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
"simple-git-hooks": "^2.11.1",
"tsx": "^4.19.1",
"typescript": "^5.6.2",
"vite": "^5.4.8",
"rolldown-vite": "https://pkg.pr.new/rolldown/vite@43",
"vitest": "^2.1.1"
},
"simple-git-hooks": {
Expand Down
6 changes: 1 addition & 5 deletions packages/plugin-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,10 @@
},
"homepage": "https://github.com/vitejs/vite-plugin-react/tree/main/packages/plugin-react#readme",
"dependencies": {
"@babel/core": "^7.25.2",
"@babel/plugin-transform-react-jsx-self": "^7.24.7",
"@babel/plugin-transform-react-jsx-source": "^7.24.7",
"@types/babel__core": "^7.20.5",
"react-refresh": "^0.14.2"
},
"peerDependencies": {
"vite": "^4.2.0 || ^5.0.0"
"rolldown-vite": "https://pkg.pr.new/rolldown/vite@43"
},
"devDependencies": {
"unbuild": "^2.0.0"
Expand Down
3 changes: 0 additions & 3 deletions packages/plugin-react/src/babel.d.ts

This file was deleted.

268 changes: 21 additions & 247 deletions packages/plugin-react/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
// eslint-disable-next-line import/no-duplicates
import type * as babelCore from '@babel/core'
// eslint-disable-next-line import/no-duplicates
import type { ParserOptions, TransformOptions } from '@babel/core'
import { createFilter } from 'vite'
import { createFilter } from 'rolldown-vite'
import type {
BuildOptions,
Plugin,
PluginOption,
ResolvedConfig,
UserConfig,
} from 'vite'
} from 'rolldown-vite'
import {
addClassComponentRefreshWrapper,
addRefreshWrapper,
Expand All @@ -18,15 +13,6 @@ import {
runtimePublicPath,
} from './fast-refresh'

// lazy load babel since it's not used during build if plugins are not used
let babel: typeof babelCore | undefined
async function loadBabel() {
if (!babel) {
babel = await import('@babel/core')
}
return babel
}

export interface Options {
include?: string | RegExp | Array<string | RegExp>
exclude?: string | RegExp | Array<string | RegExp>
Expand All @@ -41,56 +27,11 @@ export interface Options {
* @default "automatic"
*/
jsxRuntime?: 'classic' | 'automatic'
/**
* Babel configuration applied in both dev and prod.
*/
babel?:
| BabelOptions
| ((id: string, options: { ssr?: boolean }) => BabelOptions)
}

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<BabelOptions['plugins'], any[]>
presets: Extract<BabelOptions['presets'], any[]>
overrides: Extract<BabelOptions['overrides'], any[]>
parserOpts: ParserOptions & {
plugins: Extract<ParserOptions['plugins'], any[]>
}
}

type ReactBabelHook = (
babelConfig: ReactBabelOptions,
context: ReactBabelHookContext,
config: ResolvedConfig,
) => void

type ReactBabelHookContext = { ssr: boolean; id: string }

export type ViteReactPluginApi = {
/**
* Manipulate the Babel options of `@vitejs/plugin-react`
*/
reactBabel?: ReactBabelHook
}

const reactCompRE = /extends\s+(?:React\.)?(?:Pure)?Component/
const refreshContentRE = /\$Refresh(?:Reg|Sig)\$\(/
const defaultIncludeRE = /\.[tj]sx?$/
const tsRE = /\.tsx?$/

export default function viteReact(opts: Options = {}): PluginOption[] {
// Provide default values for Rollup compat.
Expand All @@ -99,13 +40,7 @@ export default function viteReact(opts: Options = {}): PluginOption[] {
const jsxImportSource = opts.jsxImportSource ?? 'react'
const jsxImportRuntime = `${jsxImportSource}/jsx-runtime`
const jsxImportDevRuntime = `${jsxImportSource}/jsx-dev-runtime`
let isProduction = true
let projectRoot = process.cwd()
let skipFastRefresh = false
let runPluginOverrides:
| ((options: ReactBabelOptions, context: ReactBabelHookContext) => void)
| undefined
let staticBabelOptions: ReactBabelOptions | undefined

// Support patterns like:
// - import * as React from 'react';
Expand All @@ -114,54 +49,27 @@ export default function viteReact(opts: Options = {}): PluginOption[] {
const importReactRE = /\bimport\s+(?:\*\s+as\s+)?React\b/

const viteBabel: Plugin = {
name: 'vite:react-babel',
enforce: 'pre',
config() {
if (opts.jsxRuntime === 'classic') {
return {
esbuild: {
jsx: 'transform',
},
}
} else {
return {
esbuild: {
jsx: 'automatic',
jsxImportSource: opts.jsxImportSource,
name: 'vite:react',
config(config, env) {
const runtime = opts.jsxRuntime ?? 'automatic'
return {
oxc: {
jsx: {
runtime,
importSource: runtime === 'automatic' ? jsxImportSource : undefined,
refresh: env.command === 'serve',
development: env.command === 'serve',
},
optimizeDeps: { esbuildOptions: { jsx: 'automatic' } },
}
},
// optimizeDeps: { esbuildOptions: { jsx: 'automatic' } },
}
},
configResolved(config) {
devBase = config.base
projectRoot = config.root
isProduction = config.isProduction
skipFastRefresh =
isProduction ||
config.isProduction ||
config.command === 'build' ||
config.server.hmr === false

if ('jsxPure' in opts) {
config.logger.warnOnce(
'[@vitejs/plugin-react] jsxPure was removed. You can configure esbuild.jsxSideEffects directly.',
)
}

const hooks: ReactBabelHook[] = config.plugins
.map((plugin) => plugin.api?.reactBabel)
.filter(defined)

if (hooks.length > 0) {
runPluginOverrides = (babelOptions, context) => {
hooks.forEach((hook) => hook(babelOptions, context, config))
}
} else if (typeof opts.babel !== 'function') {
// Because hooks and the callback option can mutate the Babel options
// we only create static option in this case and re-create them
// each time otherwise
staticBabelOptions = createBabelOptions(opts.babel)
}
},
async transform(code, id, options) {
if (id.includes('/node_modules/')) return
Expand All @@ -170,17 +78,6 @@ export default function viteReact(opts: Options = {}): PluginOption[] {
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 =
Expand All @@ -191,79 +88,14 @@ export default function viteReact(opts: Options = {}): PluginOption[] {
? 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'),
)
}
}

// Avoid parsing if no special transformation is needed
if (
!plugins.length &&
!babelOptions.presets.length &&
!babelOptions.configFile &&
!babelOptions.babelrc
) {
return
}

const parserPlugins = [...babelOptions.parserOpts.plugins]

if (!filepath.endsWith('.ts')) {
parserPlugins.push('jsx')
}

if (tsRE.test(filepath)) {
parserPlugins.push('typescript')
}

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 crates 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: hasCompiler(plugins)
? false
: !isProduction && isJSX && opts.jsxRuntime !== 'classic',
parserOpts: {
...babelOptions.parserOpts,
sourceType: 'module',
allowAwaitOutsideFunction: true,
plugins: parserPlugins,
},
generatorOpts: {
...babelOptions.generatorOpts,
decoratorsBeforeExport: true,
},
plugins,
sourceMaps: true,
})

if (result) {
let code = result.code!
if (useFastRefresh) {
if (refreshContentRE.test(code)) {
code = addRefreshWrapper(code, id)
} else if (reactCompRE.test(code)) {
code = addClassComponentRefreshWrapper(code, id)
}
if (useFastRefresh) {
if (refreshContentRE.test(code)) {
code = addRefreshWrapper(code, id)
} else if (reactCompRE.test(code)) {
code = addClassComponentRefreshWrapper(code, id)
}
return { code, map: result.map }
return { code }
}
},
}
Expand All @@ -272,11 +104,6 @@ export default function viteReact(opts: Options = {}): PluginOption[] {
// for React 18 while it's `react-dom` for React 17. We'd need to detect
// what React version the user has installed.
const dependencies = ['react', jsxImportDevRuntime, jsxImportRuntime]
const staticBabelPlugins =
typeof opts.babel === 'object' ? opts.babel?.plugins ?? [] : []
if (hasCompilerWithDefaultRuntime(staticBabelPlugins)) {
dependencies.push('react/compiler-runtime')
}

const viteReactRefresh: Plugin = {
name: 'vite:react-refresh',
Expand Down Expand Up @@ -342,56 +169,3 @@ const silenceUseClientWarning = (userConfig: UserConfig): BuildOptions => ({
},
},
})

const loadedPlugin = new Map<string, any>()
function loadPlugin(path: string): any {
const cached = loadedPlugin.get(path)
if (cached) return cached

const promise = import(path).then((module) => {
const value = module.default || module
loadedPlugin.set(path, value)
return value
})
loadedPlugin.set(path, promise)
return promise
}

function createBabelOptions(rawOptions?: BabelOptions) {
const babelOptions = {
babelrc: false,
configFile: false,
...rawOptions,
} as ReactBabelOptions

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

return babelOptions
}

function defined<T>(value: T | undefined): value is T {
return value !== undefined
}

function hasCompiler(plugins: ReactBabelOptions['plugins']) {
return plugins.some(
(p) =>
p === 'babel-plugin-react-compiler' ||
(Array.isArray(p) && p[0] === 'babel-plugin-react-compiler'),
)
}

// https://gist.github.com/poteto/37c076bf112a07ba39d0e5f0645fec43
function hasCompilerWithDefaultRuntime(plugins: ReactBabelOptions['plugins']) {
return plugins.some(
(p) =>
p === 'babel-plugin-react-compiler' ||
(Array.isArray(p) &&
p[0] === 'babel-plugin-react-compiler' &&
p[1]?.runtimeModule === undefined),
)
}
2 changes: 1 addition & 1 deletion playground/class-components/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { defineConfig } from 'vite'
import { defineConfig } from 'rolldown-vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
Expand Down
Loading
Loading