example of how dual packaging works in Node
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:
exports
field for "import" files (has .js
extension)exports
field for "require" files (has .js
extension)exports
field for "import" and "require" files (has .mjs
and .cjs
extensions)exports
field for "import" and "require" files (has .js
extension, but esm
is inside /esm
folder with near package.json
having "type": "module"
)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 get require() of ES modules is not supported
error (cjs requires
cjs-like file inside esm package)
import
syntax with this module in Nodenode esm/test-esm.mjs
, you will not get an error (esm imports
esm)run node esm/test-cjs.mjs
to get The requested module 'test-cjs' is a CommonJS module
error (esm imports
esm-like file inside cjs package)
require
syntax with this module in Nodenode cjs/test-cjs.js
, you will not get an error (cjs requires
cjs)if you run node cjs/test-mixed.js
or node esm/test-mixed.mjs
you will see no errors because it has correct exports
field.
if you run node cjs/test-folder.js
or node esm/test-folder.mjs
you will see no errors because it has correct exports
field, and esm
file has a package.json
near it with "type": "module"
.
To fix this, bundle your files with appropriate extensions:
"type"
in your package.json
, use .cjs
/.js
extensions, when files have commonjs
syntax, and .mjs
extension for files with esm
syntax{
"exports": {
".": {
"import": "./index.mjs",
"require": "./index.js" // or "./index.cjs"
}
}
}
"type": "commonjs"
in your package.json
, use .cjs
/.js
extensions, when files have commonjs
syntax, and .mjs
extension for files with esm
syntax{
"type": "commonjs",
"exports": {
".": {
"import": "./index.mjs",
"require": "./index.js" // or "./index.cjs"
}
}
}
"type": "module"
in your package.json
, use .cjs
extension, when files have commonjs
syntax, and .mjs
/.js
extension for files with esm
syntax{
"type": "module",
"exports": {
".": {
"import": "./index.js", // or "./index.mjs"
"require": "./index.cjs"
}
}
}
esm
files inside /esm
folder and put a package.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):
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.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\"}"