Skip to content

Commit 1227640

Browse files
committed
feat!: allow to run Babel on non js/ts extensions
1 parent e26bb67 commit 1227640

File tree

13 files changed

+1110
-158
lines changed

13 files changed

+1110
-158
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

+111-144
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',
@@ -120,7 +115,6 @@ export default function viteReact(opts: Options = {}): PluginOption[] {
120115
'this-is-undefined-in-esm': 'silent',
121116
},
122117
jsx: 'transform',
123-
jsxImportSource: opts.jsxImportSource,
124118
},
125119
}
126120
} else {
@@ -135,13 +129,10 @@ export default function viteReact(opts: Options = {}): PluginOption[] {
135129
configResolved(config) {
136130
devBase = config.base
137131
projectRoot = config.root
138-
filter = createFilter(opts.include, opts.exclude, {
139-
resolve: projectRoot,
140-
})
141132
needHiresSourcemap =
142133
config.command === 'build' && !!config.build.sourcemap
143134
isProduction = config.isProduction
144-
skipFastRefresh ||= isProduction || config.command === 'build'
135+
skipFastRefresh = isProduction || config.command === 'build'
145136

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

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

229-
let inputMap: SourceMap | undefined
230-
if (prependReactImport) {
231-
if (needHiresSourcemap) {
232-
const s = new MagicString(code)
233-
s.prepend(prependReactImportCode)
234-
code = s.toString()
235-
inputMap = s.generateMap({ hires: true, source: id })
236-
} else {
237-
code = prependReactImportCode + code
238-
}
204+
let inputMap: SourceMap | undefined
205+
if (prependReactImport) {
206+
if (needHiresSourcemap) {
207+
const s = new MagicString(code)
208+
s.prepend(prependReactImportCode)
209+
code = s.toString()
210+
inputMap = s.generateMap({ hires: true, source: id })
211+
} else {
212+
code = prependReactImportCode + code
239213
}
214+
}
240215

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

258-
const parserPlugins: typeof babelOptions.parserOpts.plugins = [
259-
...babelOptions.parserOpts.plugins,
260-
'importMeta',
261-
// This plugin is applied before esbuild transforms the code,
262-
// so we need to enable some stage 3 syntax that is supported in
263-
// TypeScript and some environments already.
264-
'topLevelAwait',
265-
'classProperties',
266-
'classPrivateProperties',
267-
'classPrivateMethods',
268-
]
225+
const parserPlugins: typeof babelOptions.parserOpts.plugins = [
226+
...babelOptions.parserOpts.plugins,
227+
'importMeta',
228+
// This plugin is applied before esbuild transforms the code,
229+
// so we need to enable some stage 3 syntax that is supported in
230+
// TypeScript and some environments already.
231+
'topLevelAwait',
232+
'classProperties',
233+
'classPrivateProperties',
234+
'classPrivateMethods',
235+
]
236+
237+
if (!filepath.endsWith('.ts')) {
238+
parserPlugins.push('jsx')
239+
}
269240

270-
if (!extension.endsWith('.ts')) {
271-
parserPlugins.push('jsx')
272-
}
241+
if (/\.tsx?$/.test(filepath)) {
242+
parserPlugins.push('typescript')
243+
}
273244

274-
if (/\.tsx?$/.test(extension)) {
275-
parserPlugins.push('typescript')
276-
}
245+
const result = await babel.transformAsync(code, {
246+
...babelOptions,
247+
root: projectRoot,
248+
filename: id,
249+
sourceFileName: filepath,
250+
parserOpts: {
251+
...babelOptions.parserOpts,
252+
sourceType: 'module',
253+
allowAwaitOutsideFunction: true,
254+
plugins: parserPlugins,
255+
},
256+
generatorOpts: {
257+
...babelOptions.generatorOpts,
258+
decoratorsBeforeExport: true,
259+
},
260+
plugins,
261+
sourceMaps: true,
262+
// Vite handles sourcemap flattening
263+
inputSourceMap: inputMap ?? (false as any),
264+
})
277265

278-
const result = await babel.transformAsync(code, {
279-
...babelOptions,
280-
root: projectRoot,
281-
filename: id,
282-
sourceFileName: filepath,
283-
parserOpts: {
284-
...babelOptions.parserOpts,
285-
sourceType: 'module',
286-
allowAwaitOutsideFunction: true,
287-
plugins: parserPlugins,
288-
},
289-
generatorOpts: {
290-
...babelOptions.generatorOpts,
291-
decoratorsBeforeExport: true,
292-
},
293-
plugins,
294-
sourceMaps: true,
295-
// Vite handles sourcemap flattening
296-
inputSourceMap: inputMap ?? (false as any),
297-
})
298-
299-
if (result) {
300-
let code = result.code!
301-
if (useFastRefresh && refreshContentRE.test(code)) {
302-
code = addRefreshWrapper(code, id)
303-
}
304-
return {
305-
code,
306-
map: result.map,
307-
}
266+
if (result) {
267+
let code = result.code!
268+
if (useFastRefresh && refreshContentRE.test(code)) {
269+
code = addRefreshWrapper(code, id)
308270
}
271+
return { code, map: result.map }
309272
}
310273
},
311274
}
@@ -374,3 +337,7 @@ function createBabelOptions(rawOptions?: BabelOptions) {
374337
function defined<T>(value: T | undefined): value is T {
375338
return value !== undefined
376339
}
340+
341+
function arraify<T>(target: T | T[]): T[] {
342+
return Array.isArray(target) ? target : [target]
343+
}

0 commit comments

Comments
 (0)