Learning by fixing: Node.js, modules and packages

The mystery

The project I was working on has a pretty standard frontend setup: React, Next.js, css-in-js, UI components from an external library to build the interface. One day I added a new component, and although everything worked locally, my CI, while attempting to build the website, greeted me with this:

  • it was working locally because the version of Node in the CI was newer than on my local machine.

ESM or CJS

The very first step of playing a detective would be to take a closer look at the crime scene and extract all available clues from there.

import React from 'react';
const React = require('react').default;

Modules resolution and packages

Now, that we have an idea of where the problem occurs (CJS module in Lozenge tries to require ESM module from Compiled), it’s time to dig deeper and understand why exactly this happens. First of all, let’s check out what is happening in the problematic file /dist/cjs/Lozenge/Container.js:

...var _react = _interopRequireDefault(require("react"));

var _runtime = require("@compiled/react/runtime");

var _constants = require("@atlaskit/theme/constants");
...
  • It is required as a deep import from @compiled/react package
  • dist folder, with CJS and ESM code for the entire package
  • runtime folder, also with package.json file. This one looks weird, it only has a few fields, and all of them are relative links to files inside dist folder. No name, version, etc
{
"main": "../dist/cjs/runtime.js",
"module": "../dist/esm/runtime.js",
"browser": "../dist/browser/runtime.js",
"types": "../dist/esm/runtime.d.ts"
}
  • /node_modules/@atlaskit/lozenge/dist/cjs
  • /node_modules/@atlaskit/lozenge/dist
  • /node_modules/@atlaskit/lozenge
{
"main": "../dist/cjs/runtime.js",
"module": "../dist/esm/runtime.js",
"browser": "../dist/browser/runtime.js",
"types": "../dist/esm/runtime.d.ts"
}
  • both CJS and ESM code is there
  • all the packages are on their right places and according to node resolution algorithm correct code should be used
  • the build fails on the new version of Node but works on the older one

Package exports

Although “ESM support by default” sounds like good news, it’s not that helpful in reality: that means that the change that breaks the build could be introduced in any previous version and was just hidden until now. On the other hand, it proves that:

  • and since we verified that all the fields and code are correct, then there has to be something explicit in Compiled, related to ESM, that overrides the default node behaviour
"exports": {
".": "./dist/esm/index.js",
"./babel-plugin": "./dist/esm/babel-plugin.js",
"./runtime": "./dist/esm/runtime.js"
},
  • If there is a package.json available in the path that is a combination of scope + name (@compiled/react), then it extracts exports from it and tries to match it with the required path
"exports": {
...
"./runtime": {
"import": "./dist/esm/runtime.js",
"require": "./dist/cjs/runtime.js"
}
}
  • parse node_modules/@compiled/react/package.json file
  • extract exports field from it
  • match ./runtime entry with our requested path
  • detect that the request comes from CJS file and resolve the correct ./dist/cjs/runtime.js file
  • parse node_modules/@compiled/react/runtime/package.json file
  • extract main field from it
  • resolve the correct ../dist/cjs/runtime.js file

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Nadia Makarevich

Nadia Makarevich

Frontend architect, coder. Love solving problems, fixing things and writing in-depth tech articles: https://www.developerway.com