The Mysterious Case of Emotion and “exports is not defined”
Thought I’d share a bug I ran into a while back that sent me on a Poirot style investigation full of red herrings and unexpected culprits.
This is tangentially related to my recent page speed woes at work. We’d started using Emotion for CSS-in-JS in our component library and, combined with lazy and conditional component loading, it helped with some of the “Reduce unused CSS” warnings we were seeing in Lighthouse.
So adding Emotion as a styling option in our main codebase seemed like an obvious choice. We’d already installed @emotion/core
(v10) when we started importing from our component library which meant it should be a quick, two-step process: 1) running yarn add @emotion/babel-preset-css-prop
and 2) adding it to our babel.config.js
presets after @babel/preset-react
. I followed those steps, ran Webpack, and promptly got the error “ReferenceError: exports is not defined”.
Weird.
That sent me on a lengthy wild goose chase. Stack Overflow had one question with no accepted answer. Babel was the primary suspect so I tried reordering all my presets and then upgrading to the latest version. I poked around in the Webpack config. The Emotion repo’s issues page wasn’t any help. I tried completely switching up our Emotion implementation but that created a whole new set of problems.
After hours of running around in circles, I finally went back to that first Stack Overflow link. What did my babel.config.js
have in common with the babel.config.js
posted there? The @babel/plugin-transform-modules-commonjs
plugin. Searching for @babel/plugin-transform-modules-commonjs
and “exports is not defined” got a ton of hits and revealed that @babel/plugin-transform-modules-commonjs
is a pretty common answer to the question “How do I fix an ‘exports is not defined’ error?” Finally a clue! Now why did it stop working?
It turns out plugin/preset order can be pretty important in Babel. @babel/plugin-transform-modules-commonjs
needed to run after @emotion/babel-preset-css-prop
but plugins always run first. Since presets are just collections of plugins, I tried uninstalling @emotion/babel-preset-css-prop
, looking at its source code, and installing each plugin individually. So my babel.config.js
went from looking something like this:
{
presets: [
...
'@babel/preset-react',
'@emotion/babel-preset-css-prop'
],
plugins: [
...
'@babel/plugin-transform-modules-commonjs'
]
}
to more like:
{
presets: [
...
'@babel/preset-react'
],
plugins: [
...
[
'@emotion/babel-plugin-jsx-pragmatic',
{
export: 'jsx',
module: '@emotion/core',
import: '___EmotionJSX'
}
],
[
'@babel/plugin-transform-react-jsx',
{
pragma: '___EmotionJSX',
pragmaFrag: 'React.Fragment'
}
],
'emotion',
'@babel/plugin-transform-modules-commonjs'
]
}
And that solved the mystery. It took a while but I did learn a lot more about Babel.