Skip to content

Commit

Permalink
feat!: allow to run Babel on non js/ts extensions (#122)
Browse files Browse the repository at this point in the history
  • Loading branch information
ArnaudBarre authored Apr 1, 2023
1 parent e93cf8b commit 5d8a052
Show file tree
Hide file tree
Showing 13 changed files with 1,110 additions and 149 deletions.
38 changes: 23 additions & 15 deletions packages/plugin-react/README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# @vitejs/plugin-react [![npm](https://img.shields.io/npm/v/@vitejs/plugin-react.svg)](https://npmjs.com/package/@vitejs/plugin-react)

The all-in-one Vite plugin for React projects.
The default Vite plugin for React projects.

- enable [Fast Refresh](https://www.npmjs.com/package/react-refresh) in development
- use the [automatic JSX runtime](https://legacy.reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html)
- dedupe the `react` and `react-dom` packages
- use custom Babel plugins/presets
- small installation size

```js
// vite.config.js
Expand All @@ -17,32 +17,38 @@ export default defineConfig({
})
```

## Filter which files use Fast Refresh
## Options

By default, Fast Refresh is used by files ending with `.js`, `.jsx`, `.ts`, and `.tsx`, except for files with a `node_modules` parent directory.
### include/exclude

In some situations, you may not want a file to act as a HMR boundary, instead preferring that the changes propagate higher in the stack before being handled. In these cases, you can provide an `include` and/or `exclude` option, which can be a regex, a [picomatch](https://github.com/micromatch/picomatch#globbing-features) pattern, or an array of either. Files matching `include` and not `exclude` will use Fast Refresh. The defaults are always applied.
Includes `.js`, `.jsx`, `.ts` & `.tsx` by default. This option can be used to add fast refresh to `.mdx` files:

```js
react({
// Exclude storybook stories
exclude: /\.stories\.(t|j)sx?$/,
// Only .tsx files
include: '**/*.tsx',
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import mdx from '@mdx-js/rollup'

export default defineConfig({
plugins: [
{ enforce: 'pre', ...mdx() },
react({ include: /\.(mdx|js|jsx|ts|tsx)$/ }),
],
})
```

### Configure the JSX import source
> `node_modules` are never processed by this plugin (but esbuild will)
### jsxImportSource

Control where the JSX factory is imported from. For TS projects this is inferred from the tsconfig.
Control where the JSX factory is imported from. For TS projects this is inferred from the tsconfig. If you have some React code outside JSX/TSX files, this will be used to detect the presence of React code and apply Fast Refresh.

```js
react({ jsxImportSource: '@emotion/react' })
```

## Babel configuration
### babel

The `babel` option lets you add plugins, presets, and [other configuration](https://babeljs.io/docs/en/options) to the Babel transformation performed on each JSX/TSX file.
The `babel` option lets you add plugins, presets, and [other configuration](https://babeljs.io/docs/en/options) to the Babel transformation performed on each included file.

```js
react({
Expand All @@ -58,7 +64,9 @@ react({
})
```

### Proposed syntax
Note: When not using plugins, only esbuild is used for production builds, resulting in faster builds.

#### Proposed syntax

If you are using ES syntax that are still in proposal status (e.g. class properties), you can selectively enable them with the `babel.parserOpts.plugins` option:

Expand Down
236 changes: 102 additions & 134 deletions packages/plugin-react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,6 @@ import {
export interface Options {
include?: string | RegExp | Array<string | RegExp>
exclude?: string | RegExp | Array<string | RegExp>
/**
* Enable `react-refresh` integration. Vite disables this in prod env or build mode.
* @default true
*/
fastRefresh?: boolean
/**
* @deprecated All tools now support the automatic runtime, and it has been backported
* up to React 16. This allows to skip the React import and can produce smaller bundlers.
Expand Down Expand Up @@ -83,32 +78,28 @@ declare module 'vite' {

const prependReactImportCode = "import React from 'react'; "
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.
let devBase = '/'
let filter = createFilter(opts.include, opts.exclude)
const filter = createFilter(opts.include ?? defaultIncludeRE, opts.exclude)
let needHiresSourcemap = false
let isProduction = true
let projectRoot = process.cwd()
let skipFastRefresh = opts.fastRefresh === false
const skipReactImport = false
let skipFastRefresh = false
let runPluginOverrides:
| ((options: ReactBabelOptions, context: ReactBabelHookContext) => void)
| undefined
let staticBabelOptions: ReactBabelOptions | undefined

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

// 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+)/

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

const viteBabel: Plugin = {
name: 'vite:react-babel',
enforce: 'pre',
Expand All @@ -117,7 +108,6 @@ export default function viteReact(opts: Options = {}): PluginOption[] {
return {
esbuild: {
jsx: 'transform',
jsxImportSource: opts.jsxImportSource,
},
}
} else {
Expand All @@ -132,13 +122,10 @@ export default function viteReact(opts: Options = {}): PluginOption[] {
configResolved(config) {
devBase = config.base
projectRoot = config.root
filter = createFilter(opts.include, opts.exclude, {
resolve: projectRoot,
})
needHiresSourcemap =
config.command === 'build' && !!config.build.sourcemap
isProduction = config.isProduction
skipFastRefresh ||= isProduction || config.command === 'build'
skipFastRefresh = isProduction || config.command === 'build'

if (opts.jsxRuntime === 'classic') {
config.logger.warnOnce(
Expand All @@ -164,135 +151,116 @@ export default function viteReact(opts: Options = {}): PluginOption[] {
}
},
async transform(code, id, options) {
if (id.includes('/node_modules/')) return

const [filepath] = id.split('?')
if (!filter(filepath)) return

const ssr = options?.ssr === true
// File extension could be mocked/overridden in querystring.
const [filepath, querystring = ''] = id.split('?')
const [extension = ''] =
querystring.match(fileExtensionRE) ||
filepath.match(fileExtensionRE) ||
[]

if (/\.(?:mjs|[tj]sx?)$/.test(extension)) {
const isJSX = extension.endsWith('x')
const isNodeModules = id.includes('/node_modules/')
const isProjectFile =
!isNodeModules && (id[0] === '\0' || id.startsWith(projectRoot + '/'))

const babelOptions = (() => {
if (staticBabelOptions) return staticBabelOptions
const newBabelOptions = createBabelOptions(
typeof opts.babel === 'function'
? opts.babel(id, { ssr })
: opts.babel,
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 =
!skipFastRefresh &&
!ssr &&
(isJSX ||
(opts.jsxRuntime === 'classic'
? code.includes(
`${opts.jsxImportSource ?? 'react'}/jsx-dev-runtime`,
)
: importReactRE.test(code)))
if (useFastRefresh) {
plugins.push([
await loadPlugin('react-refresh/babel'),
{ skipEnvCheck: true },
])
}

let prependReactImport = false
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'),
)
runPluginOverrides?.(newBabelOptions, { id, ssr })
return newBabelOptions
})()

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

let useFastRefresh = false
if (!skipFastRefresh && !ssr && !isNodeModules) {
// Modules with .js or .ts extension must import React.
const isReactModule = isJSX || importReactRE.test(code)
if (isReactModule && filter(id)) {
useFastRefresh = true
plugins.push([
await loadPlugin('react-refresh/babel'),
{ skipEnvCheck: true },
])
}
}

let prependReactImport = false
if (!isProjectFile || isJSX) {
if (!useAutomaticRuntime && isProjectFile) {
// These plugins are only needed for the classic runtime.
if (!isProduction) {
plugins.push(
await loadPlugin('@babel/plugin-transform-react-jsx-self'),
await loadPlugin('@babel/plugin-transform-react-jsx-source'),
)
}

// Even if the automatic JSX runtime is not used, we can still
// inject the React import for .jsx and .tsx modules.
if (!skipReactImport && !importReactRE.test(code)) {
prependReactImport = true
}
}
// Even if the automatic JSX runtime is not used, we can still
// inject the React import for .jsx and .tsx modules.
if (!importReactRE.test(code)) {
prependReactImport = true
}
}

let inputMap: SourceMap | undefined
if (prependReactImport) {
if (needHiresSourcemap) {
const s = new MagicString(code)
s.prepend(prependReactImportCode)
code = s.toString()
inputMap = s.generateMap({ hires: true, source: id })
} else {
code = prependReactImportCode + code
}
let inputMap: SourceMap | undefined
if (prependReactImport) {
if (needHiresSourcemap) {
const s = new MagicString(code)
s.prepend(prependReactImportCode)
code = s.toString()
inputMap = s.generateMap({ hires: true, source: id })
} else {
code = prependReactImportCode + code
}
}

// Plugins defined through this Vite plugin are only applied
// to modules within the project root, but "babel.config.js"
// files can define plugins that need to be applied to every
// module, including node_modules and linked packages.
const shouldSkip =
!plugins.length &&
!babelOptions.configFile &&
!(isProjectFile && babelOptions.babelrc)

// Avoid parsing if no plugins exist.
if (shouldSkip) {
return {
code,
map: inputMap ?? null,
}
}
// Avoid parsing if no special transformation is needed
if (
!plugins.length &&
!babelOptions.configFile &&
!babelOptions.babelrc
) {
return { code, map: inputMap ?? null }
}

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

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

if (/\.tsx?$/.test(extension)) {
parserPlugins.push('typescript')
}
if (tsRE.test(filepath)) {
parserPlugins.push('typescript')
}

const result = await babel.transformAsync(code, {
...babelOptions,
root: projectRoot,
filename: id,
sourceFileName: filepath,
parserOpts: {
...babelOptions.parserOpts,
sourceType: 'module',
allowAwaitOutsideFunction: true,
plugins: parserPlugins,
},
generatorOpts: {
...babelOptions.generatorOpts,
decoratorsBeforeExport: true,
},
plugins,
sourceMaps: true,
// Vite handles sourcemap flattening
inputSourceMap: inputMap ?? (false as any),
})

if (result) {
let code = result.code!
if (useFastRefresh && refreshContentRE.test(code)) {
code = addRefreshWrapper(code, id)
}
return {
code,
map: result.map,
}
const result = await babel.transformAsync(code, {
...babelOptions,
root: projectRoot,
filename: id,
sourceFileName: filepath,
parserOpts: {
...babelOptions.parserOpts,
sourceType: 'module',
allowAwaitOutsideFunction: true,
plugins: parserPlugins,
},
generatorOpts: {
...babelOptions.generatorOpts,
decoratorsBeforeExport: true,
},
plugins,
sourceMaps: true,
// Vite handles sourcemap flattening
inputSourceMap: inputMap ?? (false as any),
})

if (result) {
let code = result.code!
if (useFastRefresh && refreshContentRE.test(code)) {
code = addRefreshWrapper(code, id)
}
return { code, map: result.map }
}
},
}
Expand Down
Loading

0 comments on commit 5d8a052

Please sign in to comment.