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.

Comments