vite
vite copied to clipboard
feat: transform jsx with esbuild instead of babel
Description
Resolves https://github.com/vitejs/vite/issues/9429
Additional context
Development JSX transforms
esbuild supports dev-only JSX transforms (i.e. jsxDev: true) but this only works when using the automatic runtime. To preserve this functionality with the classic runtime, @babel/plugin-transform-react-jsx-self and @babel/plugin-transform-react-jsx-source will still be used in development when using the classic runtime.
JSX pure
https://github.com/vitejs/vite/pull/7088 added a jsxPure option to opt out of /* @__PURE__ */ annotations for transformed JSX call expressions.
On the other hand, esbuild always applies /* @__PURE__ */ annotations and has no option to configure this.
As currently implemented, this PR could possibly result in a breaking change if jsxPure: false is used.
What is the purpose of this pull request?
- [ ] Bug fix
- [x] New Feature
- [ ] Documentation update
- [ ] Other
Before submitting the PR, please make sure you do the following
- [x] Read the Contributing Guidelines.
- [x] Read the Pull Request Guidelines and follow the Commit Convention.
- [x] Check that there isn't already a PR that solves the problem the same way to avoid creating a duplicate.
- [x] Provide a description in this PR that addresses what the PR is solving, or reference the issue that it solves (e.g.
fixes #123). - [x] Ideally, include relevant tests that fail without this PR but pass with it.
Thanks for the PR @rtsao! The change looks good to me.
About jsxPure, I think we could remove the option and force it to be always true for runtime classic.
@aleclarson @frandiox @cyco130 would you check how this PR works in your projects? I think we could release it in a new major for plugin-react.
This doesn't work right now: It results in syntax errors because it leaves JSX untransformed in some cases. It seems to be because of the restoreJsx plugin: The Babel JSX plugin used to come after it, but the ESBuild transform seems to come before. ESBuild does JSX => JS and (in some cases) restoreJsx does JS => JSX
I'll look into it a bit more and try to create a reproduction but I don't see a quick fix (apart from removing the restoreJsx plugin, which works).
Turns out it requires an uncommon combination: a React component in node_modules, excluded from optimizeDeps. I've been looking but I haven't been able to find a React component package that works when excluded from optimizeDeps (most of them choke on the prop-types import).
restoreJsx doesn't kick in for optimized deps because the optimized code is too garbled for it to detect React.createElement calls with a regexp. So in the common case it wasn't doing its job anyway. Maybe we should just remove it along with this PR?
TLDR: We have two problems. 1. restoreJsx is not working most of the time because it can't parse optimized deps. 2. restoreJsx breaks this PR under rare circumstances. It would break more often if problem 1 didn't exist.
Repro tomorrow.
I have created a vite plugin for this purpose.
I made some changes, removed restoreJSX and adjusted the execution order, now esbuild->react plugin.
Now it can work normally at present also a lot faster
I have created a vite plugin for this purpose.
I made some changes, removed
restoreJSXand adjusted the execution order, now esbuild->react plugin.Now it can work normally at present also a lot faster
I confirm, using this plugin, dev server does feels faster. Will try and put comparison numbers tomorrow.
Any blocker on this PR?
Trying to follow PR progress, anybody could shed light on what is still missing? Thx for the great work!
The problem is briefly:
- The
restoreJsxplugin compilesReact.createElementcalls back to JSX. - Without this PR, Babel compiles that JSX back to JS, so everything works.
- But this PR uses Vite's internal ESBuild transform that is applied before
restoreJsxso we end up sending JSX to the browser.
I don't know of a way to run plugin transforms before ESBuild (it's already marked pre). The only way I can do is to disable restoreJsx, which works, but we lose that feature in that case. Proper fix would be to implement restoreJsx as an ESBuild plugin but that's beyond my capabilities.
Proper fix would be to implement
restoreJsxas an ESBuild plugin but that's beyond my capabilities.
I don't think that's a viable approach, as we always want Babel plugins to run before Vite's internal ESBuild plugin, so that Babel plugins have access to the JSX AST, instead of the transpiled JS AST.
The problem is briefly:
- The
restoreJsxplugin compilesReact.createElementcalls back to JSX.- Without this PR, Babel compiles that JSX back to JS, so everything works.
- But this PR uses Vite's internal ESBuild transform that is applied before
restoreJsxso we end up sending JSX to the browser.
It seems there is no test covering this issue. What is needed to reproduce it? Just some dependency with pre-compiled JSX?
Can restoreJsx just be removed? It sounds like this might not be working fully in the first place?
Here's a repo that demonstrates the issue.
I'll copy some info here for convenience. The Before this PR section explains why I think it's fine to remove the restoreJsx transform, it wasn't working for the most common case anyway.
Before this PR
restoreJsx doesn't work on optimized deps
It works as expected if the dep is added to optimizeDeps.exclude but it doesn't work for optimized deps (the common case) because the parseReactAlias function looks for a simple require("react") or import ... from "react" pattern but optimized deps look more like this:
import { __toESM, require_react } from "./chunk-HJ2HJDBH.js";
var import_react = __toESM(require_react());
// ...
import_react.default.createElement(/* ... */);
// ...
var import_react2 = __toESM(require_react());
// ...
import_react2.default.createElement(/* ... */);
and to me this looks exceedingly difficult to parse correctly and in a fool-proof way.
After this PR
restoreJsx result is thrown away
It still doesn't work for optimized deps as before. But now, it still doesn't work even if we add it to optimizeDeps.exclude. The transform is applied, but the result is thrown away because the shouldSkip test checks for plugins.length and it will be zero for non-project files (previously it would include at least the JSX transform itself).
restoreJsx is applied late
You can still make the restoreJsx transform work. There are a few ways:
- Add the dep to
optimizeDeps.excludeand add a Babel config file (evenmodule.exports = {}is enough) - Add the dep to
optimizeDeps.excludein a monorepo (so it's considered a project file) and a plugin is added
In this case restoreJsx is applied too late (after ESBuild's JSX transform) and will compile React.createElement calls back to JSX, which will never be compiled again. And obviously there will be errors in various stages, like the one below:
[vite] Internal server error: Failed to parse source for import analysis because the content contains invalid JS syntax. If you are using JSX, make sure to name the file with the .jsx or .tsx extension.
Maybe we could find a way to run restoreJSX before the optimizer 🤔
Thanks for the clear explanation @cyco130! I've removed the restoreJsx logic as it isn't really working in the first place.
Unless there's disagreement, I think it would make sense to address properly implementing this functionality in a separate PR as there are some fundamental issues that need to be dealt with.
Maybe we could find a way to run
restoreJSXbefore the optimizer 🤔
Isn't removing Babel one of the main goals here though?
Maybe we could find a way to run
restoreJSXbefore the optimizer 🤔Isn't removing Babel one of the main goals here though?
Yeah that's fair. I'm fine with the decision to remove restoreJSX. It can be a Vite plugin in the future.
We still need to determine if Babel plugins are applied before ESBuild though. Otherwise, custom Babel plugins won't be able to manipulate the JSX AST.
We still need to determine if Babel plugins are applied before ESBuild though. Otherwise, custom Babel plugins won't be able to manipulate the JSX AST.
They do, this is how the JSX self and source transforms work (which need to operate on raw JSX AST):
https://github.com/vitejs/vite/blob/ee396ba9ca9d48023e80206df4d571da7f652ff1/packages/plugin-react/src/index.ts#L245-L246
Excited to see this one coming together. Thx to all for your work!
It would be interesting to know your thoughts on the overall performance-related direction (given this ticket is about performance) in this discussion
@o-alexandrov answered there. We're going to provide guidance to use the current plugin for React or an SWC-based version when we release Vite 4. By using SWC, you are trading off flexibility (not being able to use babel plugins) and package size (~3x bigger) for HMR and cold start speed. The best option depends on the size of your project, and what libraries and patterns your project is using.
Is SWC a must for what the plugin needs? HMR with just esbuild is still impossible, right?
@silverwind, yes, it is out of the scope of esbuild at this point: https://github.com/evanw/esbuild/issues/151 We need Babel, or SWC as used by @ArnaudBarre to create https://github.com/ArnaudBarre/vite-plugin-swc-react-refresh