I faced a problem recently. I have a reference data package which is an npm package that can be consumed by two different projects:
- The frontend: a React application bundled with Vite.js, an ESM builder (with no CJS support).
- The backend: a Nestjs application that only supports CommonJS.
Also, this package contains some big files inside. Bundling everything in one file is a big no-no.
// CommonJS (CJS)
const path = require('path')
module.exports = path.extname('index.html')
// ESM (ECMAScript Modules)
import path from 'path'
export default path.extname('index.html')
Folder structure
Let’s start with a classic file architecture:
├── dist
├── src
│ ├── function-a.ts
│ ├── function-b.ts
│ └── index.ts
├── package.json
├── tsconfig.esm.json
├── tsconfig.cjs.json
└── README.md
Please note that everything under the src/
folder import/export dependencies with the ES7 import
/ export
keyword.
Library’s package.json
The solution I found is to have 2 folders under the dist
folder:
{
"name": "@maxpou/my-cool-package",
"main": "dist/cjs/index.js",
"exports": {
"require": "./dist/cjs/index.js",
"import": "./dist/esm/index.js"
},
"files": ["README.md", "dist"],
"scripts": {
"build": "npm run build:cjs && npm run build:esm",
"build:cjs": "tsc -p tsconfig.cjs.json",
"build:esm": "tsc -p tsconfig.esm.json"
}
}
You can see that I use the exports
token. This allows the package owner to define multiple entry
points. A CommonJS system will use the index located in the ./dist/cjs/index.js
folder while an
ESM system will use the other index located in the esm folder.
By the way, you can do much more with the conditional exports!
tsconfig.json files
I don’t have one file but two.
// tsconfig.esm.json
{
"compilerOptions": {
"target": "esnext",
"module": "esnext", "outDir": "./dist/esm", // ...
},
"include": ["src/index.ts"],
"exclude": ["node_modules"]
}
// tsconfig.cjs.json
{
"compilerOptions": {
"target": "es2017",
"module": "commonjs", "outDir": "./dist/cjs", // ...
},
"include": ["src/index.ts"],
"exclude": ["node_modules"]
}
If your IDE or other tools need one named tsconfig.json
, you can create a master one. Then the
specific tsconfig files can extend from it.
Then you can npm publish
and call it a day 🥳
Usage
Your package will be available in both ESM and CommonJS environments. Your users can use it without knowing there’s a separation.
// ESM
import { myFunction } from 'my-cool-package'
// CommonJS
const { myFunction } = require('my-cool-package')