Describe the issue
@stylexjs/unplugin runs its own internal Babel pass over every StyleX file (to apply the StyleX Babel plugin) by calling @babel/core's transformAsync. That call hardcodes babelrc: false, but not configFile: false — so Babel still discovers and applies any root babel.config.js / .cjs / .mjs / .ts present in the project.
For any project with a root Babel config (Vite templates, Next.js apps, monorepos with a shared Babel config, most non-toy setups), this silently causes every plugin and preset in that root config — react-compiler, relay, module-resolver, Flow/TS stripping, etc. — to run against every StyleX file inside the StyleX pass, in addition to running normally in the consumer's main Vite/Rollup/webpack pipeline.
Symptoms range from wasted CPU (we measured ~15-30% of a Vite dev warm build going to this double-transform) to real correctness bugs (React Compiler running twice, module-resolver producing subtly different rewrites depending on which pass runs first).
There is currently no way to opt the StyleX Babel pass out of this — the transformAsync options aren't exposed on the plugin's user options at all.
Expected behavior
Consumers should be able to opt the plugin's internal Babel pass out of root-config discovery — for example via configFile: false — without having to fork or pnpm patch the plugin. Ideally the plugin would also default to configFile: false (since the plugin's caller shouldn't need the consumer's root Babel config to do its work), but that's a bigger behavioural change; the immediate fix is just to make it configurable.
Steps to reproduce
Package versions: @stylexjs/unplugin@0.19.0, @babel/core@7.x.
- Create a Vite project that uses
@stylexjs/unplugin/vite.
- Add a root
babel.config.cjs (typical for a React-Compiler / Relay project):
module.exports = {
plugins: ['babel-plugin-react-compiler', 'relay'],
};
- Add any file that imports
@stylexjs/stylex and calls stylex.create(...).
- Instrument either plugin (a
console.log in the plugin's visitor or wrapping the plugin function) to log when it runs, or profile the build.
Observed: react-compiler and relay run once during the StyleX unplugin's internal Babel pass, and again in the main Vite React pipeline. Same for any other plugin/preset in the root config.
Expected: the StyleX unplugin's internal Babel pass should not apply plugins/presets the caller didn't ask for — or at minimum should expose a way to opt out.
Test case
Any Vite + React-Compiler + StyleX project reproduces it. The direct code path is packages/@stylexjs/unplugin/src/core.js in runBabelTransform:
const result = await transformAsync(inputCode, {
babelrc: false, // only disables .babelrc* files; root babel.config.* still discovered
filename,
presets,
plugins: [...],
caller: {...},
});
Additional comments
I've opened #1791 with a proposed fix: a babel option on the plugin's user options that is spread into transformAsync before the plugin's mandatory options (so consumers can extend but not override the plugin's internals). The primary use case is babel: { configFile: false }.
Happy to iterate on API naming or approach — an alternative would be to default configFile: false inside the plugin and have consumers opt back in if they want the root config applied. That's more aggressive but arguably more correct. Flagging both for maintainer preference.
We've been running the patched version at Mixcloud for a few weeks and it cleanly resolves both the perf and correctness issues.
Describe the issue
@stylexjs/unpluginruns its own internal Babel pass over every StyleX file (to apply the StyleX Babel plugin) by calling@babel/core'stransformAsync. That call hardcodesbabelrc: false, but notconfigFile: false— so Babel still discovers and applies any rootbabel.config.js/.cjs/.mjs/.tspresent in the project.For any project with a root Babel config (Vite templates, Next.js apps, monorepos with a shared Babel config, most non-toy setups), this silently causes every plugin and preset in that root config —
react-compiler,relay,module-resolver, Flow/TS stripping, etc. — to run against every StyleX file inside the StyleX pass, in addition to running normally in the consumer's main Vite/Rollup/webpack pipeline.Symptoms range from wasted CPU (we measured ~15-30% of a Vite dev warm build going to this double-transform) to real correctness bugs (React Compiler running twice,
module-resolverproducing subtly different rewrites depending on which pass runs first).There is currently no way to opt the StyleX Babel pass out of this — the
transformAsyncoptions aren't exposed on the plugin's user options at all.Expected behavior
Consumers should be able to opt the plugin's internal Babel pass out of root-config discovery — for example via
configFile: false— without having to fork orpnpm patchthe plugin. Ideally the plugin would also default toconfigFile: false(since the plugin's caller shouldn't need the consumer's root Babel config to do its work), but that's a bigger behavioural change; the immediate fix is just to make it configurable.Steps to reproduce
Package versions:
@stylexjs/unplugin@0.19.0,@babel/core@7.x.@stylexjs/unplugin/vite.babel.config.cjs(typical for a React-Compiler / Relay project):@stylexjs/stylexand callsstylex.create(...).console.login the plugin'svisitoror wrapping the plugin function) to log when it runs, or profile the build.Observed:
react-compilerandrelayrun once during the StyleX unplugin's internal Babel pass, and again in the main Vite React pipeline. Same for any other plugin/preset in the root config.Expected: the StyleX unplugin's internal Babel pass should not apply plugins/presets the caller didn't ask for — or at minimum should expose a way to opt out.
Test case
Any Vite + React-Compiler + StyleX project reproduces it. The direct code path is
packages/@stylexjs/unplugin/src/core.jsinrunBabelTransform:Additional comments
I've opened #1791 with a proposed fix: a
babeloption on the plugin's user options that is spread intotransformAsyncbefore the plugin's mandatory options (so consumers can extend but not override the plugin's internals). The primary use case isbabel: { configFile: false }.Happy to iterate on API naming or approach — an alternative would be to default
configFile: falseinside the plugin and have consumers opt back in if they want the root config applied. That's more aggressive but arguably more correct. Flagging both for maintainer preference.We've been running the patched version at Mixcloud for a few weeks and it cleanly resolves both the perf and correctness issues.