Cover for How to publish an npm package for ESM and CommonJS with TypeScript

How to publish an npm package for ESM and CommonJS with TypeScript

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:

Terminal window
β”œβ”€β”€ 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')

About the author

Maxence Poutord

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 @_maxpou

Recommended posts