reactjsmeteormaterial-uibabeljsemotion

Configuring MUI Components selector API with Babel in Meteor "TypeError: Cannot read property 'id' of null" on project start


I have a Meteor React project, for which I have added Material UI v5 (installation instructions), which comes with Emotion for CSS-in-JS styling:

$ meteor create --react meteor-react-mui
Created a new Meteor app in 'meteor-react-mui'.

$ cd meteor-react-mui

$ meteor npm install @mui/material @emotion/react @emotion/styled
+ @emotion/react@11.11.1
+ @emotion/styled@11.11.0
+ @mui/material@5.14.17
added 79 packages from 99 contributors and audited 193 packages in 51.3s

I would like to use the components selector API, so I installed @emotion/babel-plugin as required:

$ meteor npm install @emotion/babel-plugin
+ @emotion/babel-plugin@11.11.0
updated 1 package and audited 194 packages in 1.643s

...and then I added the configuration, as described in above docs, into my .babelrc file:

{
  "plugins": [
    [
      "@emotion",
      {
        "importMap": {
          "@mui/system": {
            "styled": {
              "canonicalImport": ["@emotion/styled", "default"],
              "styledBaseImport": ["@mui/system", "styled"]
            }
          },
          "@mui/material/styles": {
            "styled": {
              "canonicalImport": ["@emotion/styled", "default"],
              "styledBaseImport": ["@mui/material/styles", "styled"]
            }
          }
        }
      }
    ]
  ]
}

However, now when I start the project, I have this error:

$ meteor
[[[[[ ~/.../meteor-react-mui ]]]]]

=> Started proxy.                             
=> Started HMR server.                        
/.../.meteor/packages/meteor-tool/.../dev_bundle/lib/node_modules/meteor-promise/promise_server.js:218
      throw error;
      ^

TypeError: Cannot read property 'id' of null
    at InputFile.resolve (/tools/isobuild/compiler-plugin.js:403:61)
    at packages/babel-compiler.js:574:23
    at Array.some (<anonymous>)
    at requireWithPrefixes (packages/babel-compiler.js:569:26)
    at requireWithPath (packages/babel-compiler.js:487:14)
    at resolveHelper (packages/babel-compiler.js:459:24)
    at resolveHelper (packages/babel-compiler.js:450:21)
    at packages/babel-compiler.js:432:19
    at Array.forEach (<anonymous>)
    at walkHelper (packages/babel-compiler.js:431:10)
    at walkBabelRC (packages/babel-compiler.js:417:24)
    at BabelCompiler.BCp._inferHelper (packages/babel-compiler.js:514:17)
    at BabelCompiler.BCp._inferFromBabelRc (packages/babel-compiler.js:359:14)
    at BabelCompiler.BCp.inferExtraBabelOptions (packages/babel-compiler.js:333:10)
    at BabelCompiler.BCp.processOneFileForTarget (packages/babel-compiler.js:209:10)
    at packages/babel-compiler.js:123:25

Here are my dependencies in package.json:

{
  "dependencies": {
    "@babel/runtime": "^7.20.7",
    "@emotion/babel-plugin": "^11.11.0",
    "@emotion/react": "^11.11.1",
    "@emotion/styled": "^11.11.0",
    "@mui/material": "^5.14.17",
    "meteor-node-stubs": "^1.2.5",
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  }
}

Meteor release version in .meteor/release:

METEOR@2.13.3

And Meteor packages versions in .meteor/packages:

meteor-base@1.5.1             # Packages every Meteor app needs to have
mobile-experience@1.1.0       # Packages for a great mobile UX
mongo@1.16.7                   # The database Meteor supports right now
reactive-var@1.0.12            # Reactive variable for tracker

standard-minifier-css@1.9.2   # CSS minifier run for production mode
standard-minifier-js@2.8.1    # JS minifier run for production mode
es5-shim@4.8.0                # ECMAScript 5 compatibility for older browsers
ecmascript@0.16.7              # Enable ECMAScript2015+ syntax in app code
typescript@4.9.4              # Enable TypeScript syntax in .ts and .tsx modules
shell-server@0.5.0            # Server-side component of the `meteor shell` command
hot-module-replacement@0.5.3  # Update client in development without reloading the page


static-html@1.3.2             # Define static page content in .html files
react-meteor-data       # React higher-order component for reactively tracking Meteor data

Note: if I use the --typescript template (instead of the defaut --react one as shown above), I have the same issue, but with a slightly different error message:

TypeError: Cannot read property 'id' of null
    at InputFile.resolve (/tools/isobuild/compiler-plugin.js:403:61)
    at packages/babel-compiler.js:574:23
    at Array.some (<anonymous>)
    at requireWithPrefixes (packages/babel-compiler.js:569:26)
    at requireWithPath (packages/babel-compiler.js:487:14)
    at resolveHelper (packages/babel-compiler.js:459:24)
    at resolveHelper (packages/babel-compiler.js:450:21)
    at packages/babel-compiler.js:432:19
    at Array.forEach (<anonymous>)
    at walkHelper (packages/babel-compiler.js:431:10)
    at walkBabelRC (packages/babel-compiler.js:417:24)
    at TypeScriptCompiler.BCp._inferHelper (packages/babel-compiler.js:514:17)
    at TypeScriptCompiler.BCp._inferFromBabelRc (packages/babel-compiler.js:359:14)
    at TypeScriptCompiler.BCp.inferExtraBabelOptions (packages/babel-compiler.js:333:10)
    at TypeScriptCompiler.BCp.processOneFileForTarget (packages/babel-compiler.js:209:10)
    at packages/babel-compiler.js:123:25

Solution

  • It looks like simply using the full name of the @emotion/babel-plugin package in the .babelrc Babel configuration file solves the issue for Meteor build:

    {
      "plugins": [
        [
          "@emotion/babel-plugin", // Use full name, instead of just "@emotion"
          {
            // Etc.
          }
        ]
      ]
    }
    

    In a normal React and Babel stack, the short name @emotion in the Babel configuration is actually normalized automatically to that full name, hence the Material UI documentation provides only the short name:

    Babel has a name normalization phase [which] will automatically add these prefixes when loading [plugin] items.

    [...]

    • babel-plugin/babel-preset will be injected as the package name if only the @-scope name is given.

    Input: "@scope" / Normalized: "@scope/babel-plugin"

    Unfortunately, it looks like the Meteor babel-compiler wrapper package re-implements this logic, but only the most simple part:

    prefixes.push("@babel/plugin-", "babel-plugin-");
    // ...
    prefixes.push("");
    // ...
    inputFile.resolve(prefix + id, controlFilePath))
    

    Hence it is safer specifying the actual full name of the Babel plugin, instead of relying on name normalization.


    Note: BTW, while the build now works, I still had an issue displaying the web page in the browser, with this message in browser console:

    Uncaught Error: Component selectors can only be used in conjunction with @emotion/babel-plugin, the swc Emotion plugin, or another Emotion-aware compiler transform.

    I was using the components selector API like this:

    import { styled } from "@mui/material"; // Import from MUI instead of from @emotion, e.g. to benefit from theme prop
    
    const StyledDiv = styled("div")({
      // Some style...
    });
    
    const StyledParent = styled("div")({
      [StyledDiv]: { // Use the actual component as a selector
        // Some other style...
      }
    });
    

    Because of the import of styled function from the parent package @mui/material instead of the more specific @mui/material/styles as mentioned in the Babel config, the latter could not catch the import to transform. So I just needed to add the missing import path to the configuration as well:

    {
      "plugins": [
        [
          "@emotion/babel-plugin",
          {
            "importMap": {
              "@mui/system": {
                "styled": {
                  "canonicalImport": ["@emotion/styled", "default"],
                  "styledBaseImport": ["@mui/system", "styled"]
                }
              },
              "@mui/material/styles": {
                "styled": {
                  "canonicalImport": ["@emotion/styled", "default"],
                  "styledBaseImport": ["@mui/material/styles", "styled"]
                }
              },
              "@mui/material": { // Add the missing import path
                "styled": {
                  "canonicalImport": ["@emotion/styled", "default"],
                  "styledBaseImport": ["@mui/material", "styled"]
                }
              }
            }
          }
        ]
      ]
    }
    

    ...and now everything works!