Skip to content

Commit 5d8a052

Browse files
authored
feat!: allow to run Babel on non js/ts extensions (#122)
1 parent e93cf8b commit 5d8a052

File tree

13 files changed

+1110
-149
lines changed

13 files changed

+1110
-149
lines changed

packages/plugin-react/README.md

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
# @vitejs/plugin-react [![npm](https://img.shields.io/npm/v/@vitejs/plugin-react.svg)](https://npmjs.com/package/@vitejs/plugin-react)
22

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

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

1010
```js
1111
// vite.config.js
@@ -17,32 +17,38 @@ export default defineConfig({
1717
})
1818
```
1919

20-
## Filter which files use Fast Refresh
20+
## Options
2121

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

24-
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.
24+
Includes `.js`, `.jsx`, `.ts` & `.tsx` by default. This option can be used to add fast refresh to `.mdx` files:
2525

2626
```js
27-
react({
28-
// Exclude storybook stories
29-
exclude: /\.stories\.(t|j)sx?$/,
30-
// Only .tsx files
31-
include: '**/*.tsx',
27+
import { defineConfig } from 'vite'
28+
import react from '@vitejs/plugin-react'
29+
import mdx from '@mdx-js/rollup'
30+
31+
export default defineConfig({
32+
plugins: [
33+
{ enforce: 'pre', ...mdx() },
34+
react({ include: /\.(mdx|js|jsx|ts|tsx)$/ }),
35+
],
3236
})
3337
```
3438

35-
### Configure the JSX import source
39+
> `node_modules` are never processed by this plugin (but esbuild will)
40+
41+
### jsxImportSource
3642

37-
Control where the JSX factory is imported from. For TS projects this is inferred from the tsconfig.
43+
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.
3844

3945
```js
4046
react({ jsxImportSource: '@emotion/react' })
4147
```
4248

43-
## Babel configuration
49+
### babel
4450

45-
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.
51+
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.
4652

4753
```js
4854
react({
@@ -58,7 +64,9 @@ react({
5864
})
5965
```
6066

61-
### Proposed syntax
67+
Note: When not using plugins, only esbuild is used for production builds, resulting in faster builds.
68+
69+
#### Proposed syntax
6270

6371
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:
6472

packages/plugin-react/src/index.ts

Lines changed: 102 additions & 134 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,6 @@ import {
1414
export interface Options {
1515
include?: string | RegExp | Array<string | RegExp>
1616
exclude?: string | RegExp | Array<string | RegExp>
17-
/**
18-
* Enable `react-refresh` integration. Vite disables this in prod env or build mode.
19-
* @default true
20-
*/
21-
fastRefresh?: boolean
2217
/**
2318
* @deprecated All tools now support the automatic runtime, and it has been backported
2419
* up to React 16. This allows to skip the React import and can produce smaller bundlers.
@@ -83,32 +78,28 @@ declare module 'vite' {
8378

8479
const prependReactImportCode = "import React from 'react'; "
8580
const refreshContentRE = /\$Refresh(?:Reg|Sig)\$\(/
81+
const defaultIncludeRE = /\.[tj]sx?$/
82+
const tsRE = /\.tsx?$/
8683

8784
export default function viteReact(opts: Options = {}): PluginOption[] {
8885
// Provide default values for Rollup compat.
8986
let devBase = '/'
90-
let filter = createFilter(opts.include, opts.exclude)
87+
const filter = createFilter(opts.include ?? defaultIncludeRE, opts.exclude)
9188
let needHiresSourcemap = false
9289
let isProduction = true
9390
let projectRoot = process.cwd()
94-
let skipFastRefresh = opts.fastRefresh === false
95-
const skipReactImport = false
91+
let skipFastRefresh = false
9692
let runPluginOverrides:
9793
| ((options: ReactBabelOptions, context: ReactBabelHookContext) => void)
9894
| undefined
9995
let staticBabelOptions: ReactBabelOptions | undefined
10096

101-
const useAutomaticRuntime = opts.jsxRuntime !== 'classic'
102-
10397
// Support patterns like:
10498
// - import * as React from 'react';
10599
// - import React from 'react';
106100
// - import React, {useEffect} from 'react';
107101
const importReactRE = /(?:^|\n)import\s+(?:\*\s+as\s+)?React(?:,|\s+)/
108102

109-
// Any extension, including compound ones like '.bs.js'
110-
const fileExtensionRE = /\.[^/\s?]+$/
111-
112103
const viteBabel: Plugin = {
113104
name: 'vite:react-babel',
114105
enforce: 'pre',
@@ -117,7 +108,6 @@ export default function viteReact(opts: Options = {}): PluginOption[] {
117108
return {
118109
esbuild: {
119110
jsx: 'transform',
120-
jsxImportSource: opts.jsxImportSource,
121111
},
122112
}
123113
} else {
@@ -132,13 +122,10 @@ export default function viteReact(opts: Options = {}): PluginOption[] {
132122
configResolved(config) {
133123
devBase = config.base
134124
projectRoot = config.root
135-
filter = createFilter(opts.include, opts.exclude, {
136-
resolve: projectRoot,
137-
})
138125
needHiresSourcemap =
139126
config.command === 'build' && !!config.build.sourcemap
140127
isProduction = config.isProduction
141-
skipFastRefresh ||= isProduction || config.command === 'build'
128+
skipFastRefresh = isProduction || config.command === 'build'
142129

143130
if (opts.jsxRuntime === 'classic') {
144131
config.logger.warnOnce(
@@ -164,135 +151,116 @@ export default function viteReact(opts: Options = {}): PluginOption[] {
164151
}
165152
},
166153
async transform(code, id, options) {
154+
if (id.includes('/node_modules/')) return
155+
156+
const [filepath] = id.split('?')
157+
if (!filter(filepath)) return
158+
167159
const ssr = options?.ssr === true
168-
// File extension could be mocked/overridden in querystring.
169-
const [filepath, querystring = ''] = id.split('?')
170-
const [extension = ''] =
171-
querystring.match(fileExtensionRE) ||
172-
filepath.match(fileExtensionRE) ||
173-
[]
174-
175-
if (/\.(?:mjs|[tj]sx?)$/.test(extension)) {
176-
const isJSX = extension.endsWith('x')
177-
const isNodeModules = id.includes('/node_modules/')
178-
const isProjectFile =
179-
!isNodeModules && (id[0] === '\0' || id.startsWith(projectRoot + '/'))
180-
181-
const babelOptions = (() => {
182-
if (staticBabelOptions) return staticBabelOptions
183-
const newBabelOptions = createBabelOptions(
184-
typeof opts.babel === 'function'
185-
? opts.babel(id, { ssr })
186-
: opts.babel,
160+
const babelOptions = (() => {
161+
if (staticBabelOptions) return staticBabelOptions
162+
const newBabelOptions = createBabelOptions(
163+
typeof opts.babel === 'function'
164+
? opts.babel(id, { ssr })
165+
: opts.babel,
166+
)
167+
runPluginOverrides?.(newBabelOptions, { id, ssr })
168+
return newBabelOptions
169+
})()
170+
const plugins = [...babelOptions.plugins]
171+
172+
const isJSX = filepath.endsWith('x')
173+
const useFastRefresh =
174+
!skipFastRefresh &&
175+
!ssr &&
176+
(isJSX ||
177+
(opts.jsxRuntime === 'classic'
178+
? code.includes(
179+
`${opts.jsxImportSource ?? 'react'}/jsx-dev-runtime`,
180+
)
181+
: importReactRE.test(code)))
182+
if (useFastRefresh) {
183+
plugins.push([
184+
await loadPlugin('react-refresh/babel'),
185+
{ skipEnvCheck: true },
186+
])
187+
}
188+
189+
let prependReactImport = false
190+
if (opts.jsxRuntime === 'classic' && isJSX) {
191+
if (!isProduction) {
192+
// These development plugins are only needed for the classic runtime.
193+
plugins.push(
194+
await loadPlugin('@babel/plugin-transform-react-jsx-self'),
195+
await loadPlugin('@babel/plugin-transform-react-jsx-source'),
187196
)
188-
runPluginOverrides?.(newBabelOptions, { id, ssr })
189-
return newBabelOptions
190-
})()
191-
192-
const plugins = isProjectFile ? [...babelOptions.plugins] : []
193-
194-
let useFastRefresh = false
195-
if (!skipFastRefresh && !ssr && !isNodeModules) {
196-
// Modules with .js or .ts extension must import React.
197-
const isReactModule = isJSX || importReactRE.test(code)
198-
if (isReactModule && filter(id)) {
199-
useFastRefresh = true
200-
plugins.push([
201-
await loadPlugin('react-refresh/babel'),
202-
{ skipEnvCheck: true },
203-
])
204-
}
205197
}
206198

207-
let prependReactImport = false
208-
if (!isProjectFile || isJSX) {
209-
if (!useAutomaticRuntime && isProjectFile) {
210-
// These plugins are only needed for the classic runtime.
211-
if (!isProduction) {
212-
plugins.push(
213-
await loadPlugin('@babel/plugin-transform-react-jsx-self'),
214-
await loadPlugin('@babel/plugin-transform-react-jsx-source'),
215-
)
216-
}
217-
218-
// Even if the automatic JSX runtime is not used, we can still
219-
// inject the React import for .jsx and .tsx modules.
220-
if (!skipReactImport && !importReactRE.test(code)) {
221-
prependReactImport = true
222-
}
223-
}
199+
// Even if the automatic JSX runtime is not used, we can still
200+
// inject the React import for .jsx and .tsx modules.
201+
if (!importReactRE.test(code)) {
202+
prependReactImport = true
224203
}
204+
}
225205

226-
let inputMap: SourceMap | undefined
227-
if (prependReactImport) {
228-
if (needHiresSourcemap) {
229-
const s = new MagicString(code)
230-
s.prepend(prependReactImportCode)
231-
code = s.toString()
232-
inputMap = s.generateMap({ hires: true, source: id })
233-
} else {
234-
code = prependReactImportCode + code
235-
}
206+
let inputMap: SourceMap | undefined
207+
if (prependReactImport) {
208+
if (needHiresSourcemap) {
209+
const s = new MagicString(code)
210+
s.prepend(prependReactImportCode)
211+
code = s.toString()
212+
inputMap = s.generateMap({ hires: true, source: id })
213+
} else {
214+
code = prependReactImportCode + code
236215
}
216+
}
237217

238-
// Plugins defined through this Vite plugin are only applied
239-
// to modules within the project root, but "babel.config.js"
240-
// files can define plugins that need to be applied to every
241-
// module, including node_modules and linked packages.
242-
const shouldSkip =
243-
!plugins.length &&
244-
!babelOptions.configFile &&
245-
!(isProjectFile && babelOptions.babelrc)
246-
247-
// Avoid parsing if no plugins exist.
248-
if (shouldSkip) {
249-
return {
250-
code,
251-
map: inputMap ?? null,
252-
}
253-
}
218+
// Avoid parsing if no special transformation is needed
219+
if (
220+
!plugins.length &&
221+
!babelOptions.configFile &&
222+
!babelOptions.babelrc
223+
) {
224+
return { code, map: inputMap ?? null }
225+
}
254226

255-
const parserPlugins = [...babelOptions.parserOpts.plugins]
227+
const parserPlugins = [...babelOptions.parserOpts.plugins]
256228

257-
if (!extension.endsWith('.ts')) {
258-
parserPlugins.push('jsx')
259-
}
229+
if (!filepath.endsWith('.ts')) {
230+
parserPlugins.push('jsx')
231+
}
260232

261-
if (/\.tsx?$/.test(extension)) {
262-
parserPlugins.push('typescript')
263-
}
233+
if (tsRE.test(filepath)) {
234+
parserPlugins.push('typescript')
235+
}
264236

265-
const result = await babel.transformAsync(code, {
266-
...babelOptions,
267-
root: projectRoot,
268-
filename: id,
269-
sourceFileName: filepath,
270-
parserOpts: {
271-
...babelOptions.parserOpts,
272-
sourceType: 'module',
273-
allowAwaitOutsideFunction: true,
274-
plugins: parserPlugins,
275-
},
276-
generatorOpts: {
277-
...babelOptions.generatorOpts,
278-
decoratorsBeforeExport: true,
279-
},
280-
plugins,
281-
sourceMaps: true,
282-
// Vite handles sourcemap flattening
283-
inputSourceMap: inputMap ?? (false as any),
284-
})
285-
286-
if (result) {
287-
let code = result.code!
288-
if (useFastRefresh && refreshContentRE.test(code)) {
289-
code = addRefreshWrapper(code, id)
290-
}
291-
return {
292-
code,
293-
map: result.map,
294-
}
237+
const result = await babel.transformAsync(code, {
238+
...babelOptions,
239+
root: projectRoot,
240+
filename: id,
241+
sourceFileName: filepath,
242+
parserOpts: {
243+
...babelOptions.parserOpts,
244+
sourceType: 'module',
245+
allowAwaitOutsideFunction: true,
246+
plugins: parserPlugins,
247+
},
248+
generatorOpts: {
249+
...babelOptions.generatorOpts,
250+
decoratorsBeforeExport: true,
251+
},
252+
plugins,
253+
sourceMaps: true,
254+
// Vite handles sourcemap flattening
255+
inputSourceMap: inputMap ?? (false as any),
256+
})
257+
258+
if (result) {
259+
let code = result.code!
260+
if (useFastRefresh && refreshContentRE.test(code)) {
261+
code = addRefreshWrapper(code, id)
295262
}
263+
return { code, map: result.map }
296264
}
297265
},
298266
}

0 commit comments

Comments
 (0)