Skip to content

Commit 2f701d1

Browse files
committed
feat!: allow to run Babel on non js/ts extensions
1 parent e93cf8b commit 2f701d1

File tree

13 files changed

+1101
-148
lines changed

13 files changed

+1101
-148
lines changed

packages/plugin-react/README.md

+17-14
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,33 @@ 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: [mdx(), react({ include: /.mdx$/ })],
3233
})
3334
```
3435

35-
### Configure the JSX import source
36+
### jsxImportSource
3637

3738
Control where the JSX factory is imported from. For TS projects this is inferred from the tsconfig.
3839

3940
```js
4041
react({ jsxImportSource: '@emotion/react' })
4142
```
4243

43-
## Babel configuration
44+
### babel
4445

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.
46+
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.
4647

4748
```js
4849
react({
@@ -58,7 +59,9 @@ react({
5859
})
5960
```
6061

61-
### Proposed syntax
62+
Note: When not using plugins, only esbuild is used for production builds, resulting in faster builds.
63+
64+
#### Proposed syntax
6265

6366
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:
6467

packages/plugin-react/src/index.ts

+102-134
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,32 @@ declare module 'vite' {
8378

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

8783
export default function viteReact(opts: Options = {}): PluginOption[] {
8884
// Provide default values for Rollup compat.
8985
let devBase = '/'
90-
let filter = createFilter(opts.include, opts.exclude)
86+
const filter = createFilter(
87+
opts.include === undefined
88+
? defaultIncludeRE
89+
: [defaultIncludeRE, ...arraify(opts.include)],
90+
opts.exclude,
91+
)
9192
let needHiresSourcemap = false
9293
let isProduction = true
9394
let projectRoot = process.cwd()
94-
let skipFastRefresh = opts.fastRefresh === false
95-
const skipReactImport = false
95+
let skipFastRefresh = false
9696
let runPluginOverrides:
9797
| ((options: ReactBabelOptions, context: ReactBabelHookContext) => void)
9898
| undefined
9999
let staticBabelOptions: ReactBabelOptions | undefined
100100

101-
const useAutomaticRuntime = opts.jsxRuntime !== 'classic'
102-
103101
// Support patterns like:
104102
// - import * as React from 'react';
105103
// - import React from 'react';
106104
// - import React, {useEffect} from 'react';
107105
const importReactRE = /(?:^|\n)import\s+(?:\*\s+as\s+)?React(?:,|\s+)/
108106

109-
// Any extension, including compound ones like '.bs.js'
110-
const fileExtensionRE = /\.[^/\s?]+$/
111-
112107
const viteBabel: Plugin = {
113108
name: 'vite:react-babel',
114109
enforce: 'pre',
@@ -117,7 +112,6 @@ export default function viteReact(opts: Options = {}): PluginOption[] {
117112
return {
118113
esbuild: {
119114
jsx: 'transform',
120-
jsxImportSource: opts.jsxImportSource,
121115
},
122116
}
123117
} else {
@@ -132,13 +126,10 @@ export default function viteReact(opts: Options = {}): PluginOption[] {
132126
configResolved(config) {
133127
devBase = config.base
134128
projectRoot = config.root
135-
filter = createFilter(opts.include, opts.exclude, {
136-
resolve: projectRoot,
137-
})
138129
needHiresSourcemap =
139130
config.command === 'build' && !!config.build.sourcemap
140131
isProduction = config.isProduction
141-
skipFastRefresh ||= isProduction || config.command === 'build'
132+
skipFastRefresh = isProduction || config.command === 'build'
142133

143134
if (opts.jsxRuntime === 'classic') {
144135
config.logger.warnOnce(
@@ -164,135 +155,108 @@ export default function viteReact(opts: Options = {}): PluginOption[] {
164155
}
165156
},
166157
async transform(code, id, options) {
158+
if (id.includes('/node_modules/')) return
159+
160+
const [filepath] = id.split('?')
161+
if (!filter(filepath)) return
162+
167163
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,
164+
const babelOptions = (() => {
165+
if (staticBabelOptions) return staticBabelOptions
166+
const newBabelOptions = createBabelOptions(
167+
typeof opts.babel === 'function'
168+
? opts.babel(id, { ssr })
169+
: opts.babel,
170+
)
171+
runPluginOverrides?.(newBabelOptions, { id, ssr })
172+
return newBabelOptions
173+
})()
174+
const plugins = [...babelOptions.plugins]
175+
176+
const useFastRefresh = !skipFastRefresh && !ssr
177+
if (useFastRefresh) {
178+
plugins.push([
179+
await loadPlugin('react-refresh/babel'),
180+
{ skipEnvCheck: true },
181+
])
182+
}
183+
184+
let prependReactImport = false
185+
if (opts.jsxRuntime === 'classic' && filepath.endsWith('x')) {
186+
if (!isProduction) {
187+
// These development plugins are only needed for the classic runtime.
188+
plugins.push(
189+
await loadPlugin('@babel/plugin-transform-react-jsx-self'),
190+
await loadPlugin('@babel/plugin-transform-react-jsx-source'),
187191
)
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-
}
205192
}
206193

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-
}
194+
// Even if the automatic JSX runtime is not used, we can still
195+
// inject the React import for .jsx and .tsx modules.
196+
if (!importReactRE.test(code)) {
197+
prependReactImport = true
224198
}
199+
}
225200

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-
}
201+
let inputMap: SourceMap | undefined
202+
if (prependReactImport) {
203+
if (needHiresSourcemap) {
204+
const s = new MagicString(code)
205+
s.prepend(prependReactImportCode)
206+
code = s.toString()
207+
inputMap = s.generateMap({ hires: true, source: id })
208+
} else {
209+
code = prependReactImportCode + code
236210
}
211+
}
237212

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-
}
213+
// Avoid parsing if no special transformation is needed
214+
if (
215+
!plugins.length &&
216+
!babelOptions.configFile &&
217+
!babelOptions.babelrc
218+
) {
219+
return { code, map: inputMap ?? null }
220+
}
254221

255-
const parserPlugins = [...babelOptions.parserOpts.plugins]
222+
const parserPlugins= [...babelOptions.parserOpts.plugins
223+
]
256224

257-
if (!extension.endsWith('.ts')) {
258-
parserPlugins.push('jsx')
259-
}
225+
if (!filepath.endsWith('.ts')) {
226+
parserPlugins.push('jsx')
227+
}
260228

261-
if (/\.tsx?$/.test(extension)) {
262-
parserPlugins.push('typescript')
263-
}
229+
if (/\.tsx?$/.test(filepath)) {
230+
parserPlugins.push('typescript')
231+
}
264232

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-
}
233+
const result = await babel.transformAsync(code, {
234+
...babelOptions,
235+
root: projectRoot,
236+
filename: id,
237+
sourceFileName: filepath,
238+
parserOpts: {
239+
...babelOptions.parserOpts,
240+
sourceType: 'module',
241+
allowAwaitOutsideFunction: true,
242+
plugins: parserPlugins,
243+
},
244+
generatorOpts: {
245+
...babelOptions.generatorOpts,
246+
decoratorsBeforeExport: true,
247+
},
248+
plugins,
249+
sourceMaps: true,
250+
// Vite handles sourcemap flattening
251+
inputSourceMap: inputMap ?? (false as any),
252+
})
253+
254+
if (result) {
255+
let code = result.code!
256+
if (useFastRefresh && refreshContentRE.test(code)) {
257+
code = addRefreshWrapper(code, id)
295258
}
259+
return { code, map: result.map }
296260
}
297261
},
298262
}
@@ -361,3 +325,7 @@ function createBabelOptions(rawOptions?: BabelOptions) {
361325
function defined<T>(value: T | undefined): value is T {
362326
return value !== undefined
363327
}
328+
329+
function arraify<T>(target: T | T[]): T[] {
330+
return Array.isArray(target) ? target : [target]
331+
}

0 commit comments

Comments
 (0)