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!: allow to run Babel on non js/ts extensions #122

Merged
merged 3 commits into from
Apr 1, 2023
Merged
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
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
ArnaudBarre marked this conversation as resolved.
Show resolved Hide resolved

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 },
])
ArnaudBarre marked this conversation as resolved.
Show resolved Hide resolved
}

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