Single warehouse implementation to export esm and cjs at the same time
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 throughrequire('package')
, exported bymodule.exports
. This modular system is used withNodeJs
andNPM 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 supportesm
.
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.
Note
- ESM - ECMAScript modules
- CJS - CommonJs
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
.