Skip to content

Single warehouse implementation to export esm and cjs at the same time

About 1043 wordsAbout 3 min

node

2022-04-06

When developing some public modules as a standalone repository, it may sometimes be imported through import in a project using es. It is possible to import in a cjs project via require.

How to implement the ability of a single repository to be imported by cjs and esm projects at the same time?

Why do this?

In the past time, JavaScript did not have a standard modular system, and in the past time, various modular solutions have gradually developed. Among them, there are two most mainstream modular solutions:

  • CommonJs: i.e. cjs, imported through require('package'), exported by module.exports. This modular system is used with NodeJs and NPM packages.
// in cjs
const _ = require('lodash')
console.log(`assignIn: ` _.assignIn({ b: '2'}, { a: '1' }))
// { a: '1', b: '2' }
  • Ecmascript modules: esm. In 2015, esm was finally identified as a standard modular system, browsers and various communities began to gradually Migrate and support esm.
import { assignIn } from 'lodash'
console.log(`assignIn: `assignIn({ b: '2'}, { a: '1' }))
// { a: '1', b: '2' }

ESM uses named exports, which can better support static analysis, and is conducive to tree-shaking for various packaging tools. Moreover, browser native support, as a standard, represents the future of JavaScript.

At the same time, in the v12.22.0 and v14.17.0 versions of NodeJs, experimental support for ESM was started, and officially supported ESM in the 16.0.0 version.

Currently there are many packages that only support CJS or ESM formats. But at the same time, there are more and more packages that recommend and only support exporting the ESM format.

But relatively speaking, as a library, it is still too radical to support only the ESM format. Even if NodeJs v16 has officially started supporting ESM, However, the migration of the entire community still requires a lot of time and labor costs. If a certain version destroys the migration from CJS to ESM, Then it may lead to a series of problems.

Therefore, if a library can support ESM and CJS at the same time, it is a relatively safer migration solution.

Coexistence Problem

We know that Nodejs can support ESM and CJS at the same time, but one of the main problems is that it cannot be in a CJS Importing ESM will throw an error:

// cjs package
const pkg = require('esm-only-package')
Error [ERR_REQUIRE_ESM]: require() of ES Module esm-only-package not supported.

Because the ESM module is essentially an asynchronous module, it is impossible to import an asynchronous module synchronously using the require() method. However, this does not mean that the ESM module cannot be used in the CJS module at all. We can use the dynamic import() method to import the ESM module asynchronously. import() returns a Promise:

// CJS
const { default: pkg } = await import('esm-only-package')

However, this is not a satisfactory solution. It looks a bit clumsy and does not conform to the general usage habits, compared to the module import method we use every day. We still hope to import methods that meet general habits:

// ESM
import { named } from 'esm-package'
import cjs from 'cjs-package'

How to do it?

package.json

In the current stable version of NodeJs, two different formats are already supported in a package at the same time. In the package.json file, there is an exports field that provides us with conditional exports.

{
 "name": "package",
 "exports": {
 ".": {
 "require": "./index.js",
 "import": "./index.mjs"
 }
 }
}

This statement describes that when importing the default module of a package, if it is imported through require('package'), then the ./index.js file is imported. If it is imported through import pkg from 'package', then the ./index.mjs` file is imported.

Nodejs will select the appropriate import method to import the package according to the current running environment.

So we can use this feature to complete the first step in our single warehouse supporting two formats.

Then, the next thing to solve is how to build an export file in two formats.

Building

Of course it is impossible for us to write two copies of code to support both CJS and ESM.

But we can use some building packaging tools to generate ESM and CJS code.

Normally, we might use rollup to build and package our modules. Or you can also use tsup to build.

rollup

When we choose rollup to build a library, the configuration may be as follows:

// rollup.config.js
export default {
  input: 'src/index.js',
  output: {
    file: './dist/index.js',
  },
}

Since rollup supports multi-configuration packaging, we can use the multi-configuration method to package and output files in two formats at the same time:

// rollup.config.js
export default [
  {
    input: 'src/index.js',
    output: {
      file: './dist/index.js',
      format: 'cjs',
    },
  },
  {
    input: 'src/index.js',
    output: {
      file: './dist/index.mjs',
      format: 'es',
    },
  },
]

tsup

tsup is a packaging tool for TypeScript. Based on esbuild, we can easily package our library into multiple modes for output:

tsup can support zero configuration, and you can output two formats directly using the command line.

tsup src/index.ts --format esm,cjs

After execution, two files will be obtained: the cjs format file dist/index.js and the esm format file dist/index.mjs.

After building with the build tool, the next step is to improve package.json.

It is recommended to declare a standard esm library when using the type field as module, and add main, module, exports fields, For backward compatibility:

{
  "name": "my-package",
  "type": "module",
  "main": "./dist/index.js",
  "module": "./dist/index.mjs",
  "exports": {
    ".": {
      "require": "./dist/index.js",
      "import": "./dist/index.mjs"
    }
  },
  "types": "./dist/index.d.ts",
  "files": ["dist"]
}

Finally, you can import this package according to environment requirements in your CJS project or ESM project.

// cjs
const pkg = require('my-package')
// esm
import pkg from 'my-package'

Summarize

Although Nodejs has been stably supported esm from the v14.18.0 version, and to the v16 version, esm is officially supported. However, upgrading the library to only support esm is still a more radical approach. It is recommended to start migration from relatively safe dual-format support, and at the right time, transition to only support esm.