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.
We have 4 packages:
- test-cjs is a commonjs package that has incorrect
exportsfield for "import" files (has.jsextension) - test-esm is a esm pckage that has incorrect
exportsfield for "require" files (has.jsextension) - test-mixed is commonjs package that has correct
exportsfield for "import" and "require" files (has.mjsand.cjsextensions) - test-folder is a commonjs package that has correct
exportsfield for "import" and "require" files (has.jsextension, butesmis inside/esmfolder with nearpackage.jsonhaving"type": "module")
Run
pnpm installbefore running examples
When running this package, you will get an error, depending on its type field:
-
run
node cjs/test-esm.jsto getrequire() of ES modules is not supportederror (cjsrequirescjs-like file inside esm package)- which means you can only use
importsyntax with this module in Node - example: when running
node esm/test-esm.mjs, you will not get an error (esmimportsesm)
- which means you can only use
-
run
node esm/test-cjs.mjsto getThe requested module 'test-cjs' is a CommonJS moduleerror (esmimportsesm-like file inside cjs package)- which means you can only use
requiresyntax with this module in Node - example: when running
node cjs/test-cjs.js, you will not get an error (cjsrequirescjs)
- which means you can only use
-
if you run
node cjs/test-mixed.jsornode esm/test-mixed.mjsyou will see no errors because it has correctexportsfield. -
if you run
node cjs/test-folder.jsornode esm/test-folder.mjsyou will see no errors because it has correctexportsfield, andesmfile has apackage.jsonnear 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/.jsextensions, when files havecommonjssyntax, and.mjsextension for files withesmsyntax
- If your package has
"type": "commonjs"in yourpackage.json, use.cjs/.jsextensions, when files havecommonjssyntax, and.mjsextension for files withesmsyntax
{
"type": "commonjs",
"exports": {
".": {
"import": "./index.mjs",
"require": "./index.js" // or "./index.cjs"
}
}
}- If your package has
"type": "module"in yourpackage.json, use.cjsextension, when files havecommonjssyntax, and.mjs/.jsextension for files withesmsyntax
{
"type": "module",
"exports": {
".": {
"import": "./index.js", // or "./index.mjs"
"require": "./index.cjs"
}
}
}- If your bundler doesn't allow mixing extensions, you can build
esmfiles inside/esmfolder and put apackage.jsonwith"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):
export default {
input: "src/index.js",
output: [
{
file: "dist/index.cjs",
format: "cjs",
},
{
file: "dist/index.mjs",
format: "esm",
},
],
};TODO
export default {
build: {
lib: {
entry: path.resolve(__dirname, "src/index.js"),
name: "MyName",
fileName: (format) => `index.${format == "es" ? "mjs" : "js"}`,
},
},
};export default {
entry: ["src/index.js"],
format: ["esm", "cjs"],
};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.jsonThen 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\"}"- 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
{ "exports": { ".": { "import": "./index.mjs", "require": "./index.js" // or "./index.cjs" } } }