
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.
{ "compilerOptions": { "target": "esnext", "module": "esnext", "outDir": "./dist/esm", // ... }, "include": ["src/index.ts"], "exclude": ["node_modules"]}
{ "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.
// ESMimport { myFunction } from 'my-cool-package'
// CommonJSconst { myFunction } = require('my-cool-package')
About the author

Hey, I'm Maxence Poutord, a passionate software engineer. In my day-to-day job, I'm working as a senior front-end engineer at Orderfox. When I'm not working, you can find me travelling the world or cooking.
Follow me on Bluesky