Node "exports", "module" fields explanation
Node doesn't allow using .js
extension for both esm
and commonjs
files in the same project when importing files from that project. This is why this exports
field is BAD:
{
"exports": {
".": {
"import": "./index.esm.js",
"require": "./index.cjs.js"
}
}
}
Node also doesn't support "module" field by its resolution algorithm (this is bundlers convention that wasn't officially adopted by Node), so it is highly encouraged to use "exports" field alongside "module" field for the older bundlers. In the examples bellow only "exports" field is used.
Prerequisite
We have 4 packages:
- test-cjs is a commonjs package that has incorrect
exports
field for "import" files (has.js
extension) - test-esm is a esm pckage that has incorrect
exports
field for "require" files (has.js
extension) - test-mixed is commonjs package that has correct
exports
field for "import" and "require" files (has.mjs
and.cjs
extensions) - test-folder is a commonjs package that has correct
exports
field for "import" and "require" files (has.js
extension, butesm
is inside/esm
folder with nearpackage.json
having"type": "module"
)
Examples
Run
pnpm install
before running examples
When running this package, you will get an error, depending on its type
field:
-
run
node cjs/test-esm.js
to getrequire() of ES modules is not supported
error (cjsrequires
cjs-like file inside esm package)- which means you can only use
import
syntax with this module in Node - example: when running
node esm/test-esm.mjs
, you will not get an error (esmimports
esm)
- which means you can only use
-
run
node esm/test-cjs.mjs
to getThe requested module 'test-cjs' is a CommonJS module
error (esmimports
esm-like file inside cjs package)- which means you can only use
require
syntax with this module in Node - example: when running
node cjs/test-cjs.js
, you will not get an error (cjsrequires
cjs)
- which means you can only use
-
if you run
node cjs/test-mixed.js
ornode esm/test-mixed.mjs
you will see no errors because it has correctexports
field. -
if you run
node cjs/test-folder.js
ornode esm/test-folder.mjs
you will see no errors because it has correctexports
field, andesm
file has apackage.json
near it with"type": "module"
.
To fix this, bundle your files with appropriate extensions:
- If your package doesn't have
"type"
in yourpackage.json
, use.cjs
/.js
extensions, when files havecommonjs
syntax, and.mjs
extension for files withesm
syntax
{
"exports": {
".": {
"import": "./index.mjs",
"require": "./index.js" // or "./index.cjs"
}
}
}
- If your package has
"type": "commonjs"
in yourpackage.json
, use.cjs
/.js
extensions, when files havecommonjs
syntax, and.mjs
extension for files withesm
syntax
{
"type": "commonjs",
"exports": {
".": {
"import": "./index.mjs",
"require": "./index.js" // or "./index.cjs"
}
}
}
- If your package has
"type": "module"
in yourpackage.json
, use.cjs
extension, when files havecommonjs
syntax, and.mjs
/.js
extension for files withesm
syntax
{
"type": "module",
"exports": {
".": {
"import": "./index.js", // or "./index.mjs"
"require": "./index.cjs"
}
}
}
- If your bundler doesn't allow mixing extensions, you can build
esm
files inside/esm
folder and put apackage.json
with"type": "module"
inside.
package.json
in the root:
{
"exports": {
".": {
"import": "./esm/index.js",
"require": "./index.js"
}
}
}
package.json
in /esm
:
{
"type": "module"
}
Warning: These examples work with Webpack/Rollup/Vite and other bundler's pipelines because they don't run these files, they only read and analyze them, but they will FAIL when run inside Node by tools like Vitest, SSR or manually.
To build you project correctly, use one of these configs (you can see this in examples
folder):
rollup.config.js
export default {
input: "src/index.js",
output: [
{
file: "dist/index.cjs",
format: "cjs",
},
{
file: "dist/index.mjs",
format: "esm",
},
],
};
webpack.config.js
TODO
vite.config.js
export default {
build: {
lib: {
entry: path.resolve(__dirname, "src/index.js"),
name: "MyName",
fileName: (format) => `index.${format == "es" ? "mjs" : "js"}`,
},
},
};
tsup.config.js
export default {
entry: ["src/index.js"],
format: ["esm", "cjs"],
};
using tsc
This will create dist
folder with commonjs files, and esm
folder inside with esm files.
You will need two tsconfig.json
configs:
// tsconfig.cjs.json
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "commonjs",
"outDir": "dist",
"target": "es2015"
}
}
// tsconfig.esm.json
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "esnext",
"outDir": "dist/esm",
"target": "esnext"
}
}
Run these command to generate dist
:
$ tsc -p tsconfig.cjs.json
$ tsc -p tsconfig.esm.json
Then you need to put package.json
with "type": "module"
inside dist/esm
folder. You can do it in several ways, but the easiest would be running this command:
$ echo >dist/esm/package.json "{\"type\":\"module\"}"
See also
- Dual CommonJS/ES module packages in official Node.js documentation
- Publish ESM and CJS in a single package by Anthony Fu
- How to Create a Hybrid NPM Module for ESM and CommonJS on SenseDeep
- publint tool to check if package is published right, by Bjorn Lu