Skip to content

Commit

Permalink
post: publish How to publish an npm package for ESM and CommonJS with…
Browse files Browse the repository at this point in the history
… TypeScript (#164)
  • Loading branch information
maxpou authored Sep 8, 2023
1 parent 1dac0cc commit d8946e9
Show file tree
Hide file tree
Showing 3 changed files with 152 additions and 2 deletions.
Binary file added content/posts/2023/2023-09-08-cjs/cover.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
134 changes: 134 additions & 0 deletions content/posts/2023/2023-09-08-cjs/index.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
---
layout: post
title: How to publish an npm package for ESM and CommonJS with TypeScript
description:
A step by step tutorial on how to publish an npm package for ECMAScript Modules (ESM) and CommonJS
(CJS) with TypeScript
slug: publish-npm-package-esm-and-cjs
date: 2023-09-08
language: en
cover: ./cover.jpg
tags: ['JavaScript']
---

import Info from '../../../../src/components/MDX/Info'

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.

<Info title="💡 ESM? CJS? What’s that?">
<p>CommonJS (CJS) is the older and traditional way of dealing with JS dependencies.</p>
<p>ESM (ECMAScript Modules) is the official standard format to package JavaScript.</p>
</Info>

```tsx
// 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:

```bash
├── 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:

```json
{
"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](https://nodejs.org/api/packages.html#conditional-exports)!

## tsconfig.json files

I don't have one file but two.

```jsx{5-6}
// tsconfig.esm.json
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"outDir": "./dist/esm",
// ...
},
"include": ["src/index.ts"],
"exclude": ["node_modules"]
}
```

```jsx{5-6}
// 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.

```ts
// ESM
import { myFunction } from 'my-cool-package'

// CommonJS
const { myFunction } = require('my-cool-package')
```
20 changes: 18 additions & 2 deletions src/components/MDX/Info.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,24 @@ const MessageWrapper = styled.aside`
}
`

const Info = ({ children }) => {
return <MessageWrapper>{children}</MessageWrapper>
const MessageHeader = styled.div`
font-weight: bold;
& > svg {
position: relative;
top: 5px;
width: 25px;
height: 25px;
padding-right: 5px;
}
`

const Info = ({ children, title }) => {
return (
<MessageWrapper>
{title && <MessageHeader>{title}</MessageHeader>}
{children}
</MessageWrapper>
)
}

export default Info

0 comments on commit d8946e9

Please sign in to comment.