Note: CJS is used for commonJS Module in this article. Use ESM to represent es Module.

preface

For our current Web development project, we are happy to use export/import for modular development. It seems that everything is so simple

But we also know that ESM was introduced in the specification in ES6. Modularity didn’t exist in the JS language specification until now (of course, the Web wasn’t that complicated before). CJS was a product before ESM. It is the simple and efficient modular design of CJS that makes CJS popular with a large number of developers. Although the latter ESM puts forward modularity specifications at the language level, there are already a large number of CJS modules before THE emergence of ES6, so it is impossible to require all modules to migrate to ESM. The modular implementation of CJS and ESM is still very different, but in the project, it is inevitable that ESM calls CJS, so how to achieve this?

Translation translation

Esm and CJS are essentially two things, and the ESM specification only explains the behavior of es Modules, not how to accommodate CJS. The reason we can call each other happily is because the compiler has your back (there is no such thing as a quiet time, just someone who carries the load for you). The general idea is to convert ESM to CJS form and then unify the processing. First, let’s put aside Babel, rollup, and think of yourself as a compiler developer. How do you deal with ESM calls to CJS?

Suppose you have an ESM module:

//lib.js

export const a = 1;
export const b = 2;

export default() = > {return 3;
};
Copy the code

Now it’s up to you to process this code into CJS. Export const xx = commonJS exports.xx = es module export const xx = commonjs exports.xx Then there is export default. After checking the materials, we know that in ES Module, export default can be regarded as a variable or method that exports a default. Exports. default = xx (exports.default = xx)

// lib.js [cjs]
module.exports {
  a: 1.b: 2.default: function(){return 3;}
}
Copy the code

The translation process looks fine, and we happily saved the CJS version of lib.js. This is when someone used lib.js in their project in the es module form:

import fn, { a, b } from 'lib';
console.log(a);
console.log(b);
console.log(fn());
Copy the code

It happens that you still have to do the glorious translation work. Look at the title, it is not easy, just a small import, do it. The conversion was made:

const { a, b } = require('lib');
const fn = require('lib').default;
console.log(a);
console.log(b);
console.log(fn());
Copy the code

It doesn’t seem to be a problem. Because the code, logically, is exactly right. But notice the default here, which we export as an attribute of exports. Using the compiler in react projects will result in a bunch of errors if this conversion rule is used. Import react from ‘react’ would be translated as const react = require(‘react’).default; Objects exported by the React module do not have the default attribute. Also, not only react, many CJS lib exports only one method or class like this: module.exports = function() {}. In order to use the CJS library directly, you have to write: import * as React from ‘React’ to export the entire library. This is how typescript was handled before.

However, when you enable esModuleInterop or Babel in a typescript project, you import a CJS module like this: import react from ‘react’. All of this, thanks to these amazing compilers.

Babel handles ESM using CJS

Let’s go straight to Babel Online and see what it does with our lib.js:

'use strict';

Object.defineProperty(exports, '__esModule', {
  value: true
});
exports.default = exports.b = exports.a = void 0;
var a = 1;
exports.a = a;
var b = 2;
exports.b = b;
var _default = 3;
exports.default = _default;
Copy the code

Here, it seems, it’s no different than what we’re going to do. Only in exports hangs an __esModule attribute. Then we look at the import, the simplest first:

import { a, b } from 'lib';

console.log(a);
console.log(b);
Copy the code

After Babel treatment, it looks like this:

'use strict';

var _lib = require('lib');

console.log(_lib.a);
console.log(_lib.b);
Copy the code

Babel uses the whole import when working with this block. What about default import?

'use strict';

var _lib = _interopRequireDefault(require('lib'));

function _interopRequireDefault(obj) {
  return obj && obj.__esModule ? obj : { default: obj };
}

console.log(_lib.default);
Copy the code

Note here that while Babel still uses the overall import form, it wraps a layer of _interopRequireDefault that handles the default import form separately. The esModule will mount the __esModule attribute on export when it is converted, so when importing, it will return the es module directly if it is not, if CJS handles it, it will mount the whole module on the default attribute of an object. In this way, subsequent operations are unified. This is why you can use Babel as import react from ‘react’ in projects that use it.

Rollup processes ESM using CJS

Most operations are consistent when rollup converts ESM to CJS. Lib.js rollup handles this:

'use strict';

Object.defineProperty(exports, '__esModule', { value: true });

const a = 1;
const b = 2;
var module_2 = 3;

exports.a = a;
exports.b = b;
exports.default = module_2;
Copy the code

Import is handled like this:

function _interopDefault(ex) {
  return ex && typeof ex === 'object' && 'default' in ex ? ex['default'] : ex;
}

var c = require('lib');
var c__default = _interopDefault(c);

console.log(c.a);
console.log(c.b);
console.log(c__default);
Copy the code

Rollup uses the _interopDefault method when processing the default import to get the default attribute on the module. Note here that rollup itself does not use the __esModule flag to handle default, but when compiling ESM, it does so that you can happily use it in Babel projects even if you are rollup’s package.

Note that if there is only a single default export, the rollup processing will be different:

// esm
export default function add(a, b) {
  console.log(a + b);
}

// rollup handles to CJS
function add(a, b) {
  console.log(a + b);
}

module.exports = add;
Copy the code

Rollup attaches default export directly to module.exports. Babel is mounted on exports.default with an __esModule identifier. CJS users can only use require(‘lib’).default if there is a library that was previously processed using Babel. One day the author of the library decides to use rollup, so CJS users who want to use the new library have to change the original code.

The last

In order to use CJS modules in ESM happily, the more common form is to convert them to CJS for unified processing. In the process of converting ESM into CJS, the main problem is reflected in export default, which makes things complicated. In our normal development, we might be used to default export, especially in the React project, we naturally write export default myComponent. If the project is based on ES Module system, default export is definitely a very convenient way. However, if you need to support both CJS and ESM, and call each other, you should seriously consider default export. Because how esM and CJS interact successfully is not up to you, it is up to the tools that help you package the processing.

Refer to the article

  • Disable default export completely
  • Great import schism: Typescript confusion around imports explained
  • import * as React from ‘react’; vs import React from ‘react’;