---
title: "Supporting Dual Package for CJS and ESM in TypeScript Library"
date: 2023-06-14T12:56:28.000Z
canonical: https://www.ekino.fr/publications/supporting-dual-package-for-cjs-and-esm-in-typescript-library/
---

![](https://www.ekino.fr/media/original/0000/00/a6adb4e6-22dc-4767-7aaf-e5cfc932544c.png)

JavaScript is evolving rapidly. [Now, it’s really important for libraries to work with both CommonJS (CJS) and ECMAScript Modules (ESM)](https://medium.com/ekino-france/de-commonjs-%C3%A0-esm-les-cl%C3%A9s-dune-migration-r%C3%A9ussie-10507304bc01).

In this article, we’ll guide you through an easy and practical approach to handle dual-package support. That means more people can use your library, and it’s easier for them to do so.

**Understanding Javascript file extensions:**

Firstly, we need clarify the different extensions in JavaScript:

- ***.cjs***: files are for modules used with the ***require()*** function.
- ***.mjs*** files are for ESM modules, used with import statements.
- ***.js* **files can be used for both CJS and ESM modules if you specify ***“type: “module”*** in **package.json**.

**Challenges and solutions:**

Understanding which file extension corresponds to which type of module is essential but can be confusing.

When you use a specific file extension in the ***import path***, you must ensure that the corresponding extension is present in the compiled JavaScript files.

Here’s how it works:

- *.****cjs*** and *.****mjs***: If you use ***.cjs*** in the import path, the compiled JavaScript files should also have the ***.cjs*** extension. Similarly, if you use ***.mjs*** in the import path, the compiled files should have the ***.mjs*** extension. This ensures that the JavaScript engine knows which module system to use.
- .**js** : Alternatively, you can choose to use the ***.js*** extension for both CJS and ESM. However, when you do this, you need to be aware of how your code is compiled with the configuration in ***package.json***.

Based on my experiences, developers often choose to use ***.js*** for writing both ESM and CJS, picking ***.cjs*** for CJS and ***.mjs*** for ESM. In other words, if they use ***.js*** for ESM, they use ***.cjs*** for CJS, and vice versa.

Here are some examples of libraries demonstrating how developers handle this:

- [***axios***](https://github.com/axios/axios): A tool for making HTTP requests in Node.js and the browser. They use ***.js*** for ESM and ***.cjs*** for CJS. They don’t have a build step because they write code in JS with the*** .d.ts*** files included.
- [***helmet***](https://github.com/helmetjs/helmet): A tool for securing HTTP headers in Node.js. They use [***rollup***](https://github.com/rollup/rollup) to manage the build process, picking ***.cjs*** for CJS and ***.mjs*** for ESM.
- [***zod***](https://github.com/colinhacks/zod): A validation library for TypeScript. They write code in TypeScript CJS, also using [***rollup***](https://github.com/rollup/rollup) to build ESM with ***.mjs*** extension. They use TSC to build CJS with ***.js*** extension.
- [***cucumber***](https://github.com/cucumber/cucumber-js): A tool for writing tests with Gherkin syntax, we used it a lot for integration tests in our projects. They write code in TypeScript in CJS and use ***.mjs*** for ESM. They use TSC and have their own rules for building both CJS and ESM.

Okay, let’s check out the following example to understand everything better.

```
// example.ts 
// Importing with .cjs extension 
import { stringify } from './output_utils.cjs' 
import { LogColor, Log, LogLevel, Output } from './index.cjs' 

// Importing with .mjs extension 
import {stringify} from './output_utils.mjs' 
import { LogColor, Log, LogLevel, Output } from './index.mjs' 
  
// Importing with .js extension 
import {stringify} from './output_utils.js' 
import { LogColor, Log, LogLevel, Output } from './index.js' 
```

This code is written in TypeScript. We used ***“type”: “module”*** in ***package.json*** to enable ESM, which means we must specify the extension in each import path. After compilation, these extensions become crucial.

Now, let’s say you want to use different extensions like ***.cjs*** or ***.mjs*** for your imports and compilation. If the extension you specify in your import statement doesn’t match the one in the compiled JavaScript files, it can cause issues like: ***\[ERR\_MODULE\_NOT\_FOUND\]: Cannot find module***…

Choosing different extensions for imports and compilation, such as ***.cjs*** or ***.mjs***, requires careful attention. Tools like ***esbuild, swc, tsc, rollup***, or ***tsup*** …etc.. can help you compile TypeScript to JavaScript with these extensions. However, it often involves adding more scripts to modify the import paths during the build process. While this method can work, it can also be risky and challenging to maintain consistency, especially in complex projects.

**Selected solution: using .js for simplifying**

We decided to keep it simple by using ***.js*** for both importing and compiling. With the ***“type”*** field in ***package.json***, we can we can easily distinguish between CJS and ESM.

- It doesn’t require additional compilation tools, except for a quick build tool like ***esbuild***, if needed. For simpler projects, you can stick directly with TypeScript’s built-in ***TSC***.
- By using .js for everything, we make our code easier to handle and avoid any issues with file extensions.

**So how it works?**

As mentioned earlier, the important part of making this method work is the ***“type”*** field in your ***package.json*** file.

By default, if you don’t specify ***type: “commonjs”*** in your ***package.json***, your project is considered to be in CJS mode. In this mode, all **.js** files are treated as CJS modules. However, if you specify ***type: “module”***, all **.js** files are treated as ESM.

Additionally, placing another ***package.json*** file in a child folder allows you to control the scope of that folder, similar to how ***.eslintrc*** works. For example, if you have a ***package.json*** file with ***type: “module”*** in the ***lib/esm*** folder, all ***.js*** files in that folder must follow the syntax of ESM.

**Practical part**

Now, let’s explore a practical example of how to configure your project for dual-package support using scripts.

- **Modifying package.json**

```
"type": "module", 
"files": ["/lib"], 
"exports": { 
    ".": { 
        "require": { 
            "default": "./lib/cjs/index.js", 
            "types": "./lib/cjs/index.d.ts" 
        }, 
        "import": { 
            "default": "./lib/esm/index.js", 
            "types": "./lib/esm/index.d.ts" 
        } 
    } 
}
```

This configuration ensures proper support for both CJS and ESM. When using the import syntax, the library points to the ESM folder and executes code with ESM. On the other hand, when using the require syntax, the library directs to the CJS folder.

- **Compiling with TypeScript**

To set up this configuration, ensure that both the ESM and CJS folders (***lib/esm*** and ***lib/cjs***) contain the necessary library exports. We’ll achieve this using ***tsc***. 
First, adjust your ***tsconfig.json*** file by setting the ***module*** option to **“*nodenext*”**:

```
{ 
    "compilerOptions": { 
        "incremental": true, 
        "noImplicitAny": true, 
        "allowJs": true, 
        "target": "esnext", 
        "lib": ["esnext","dom"], 
        "module": "nodenext", 
        "alwaysStrict": true, 
        "skipLibCheck": true, 
        "noUnusedParameters": false, 
        "noUnusedLocals": false, 
        "strictNullChecks": true, 
        "noUncheckedIndexedAccess": true, 
        "esModuleInterop": true, 
        "allowSyntheticDefaultImports": true, 
        "forceConsistentCasingInFileNames": true, 
        "typeRoots": ["./node_modules/@types", "./@types"], 
    }, 
    "include": ["src/**/*", "test/**/*.ts"] 
}
```

This configuration is well-suited for managing TypeScript code within your project, including test files. To handle compilation specifically for npm packages, create a separate ***tsconfig.lib.json*** file that extends the original configuration:

```
{ 
    "extends": "./tsconfig.json", 
    "include": ["src/**/*.ts"], 
    "compilerOptions": { 
        "sourceMap": true, 
        "declaration": true 
    } 
}
```

- **Scripting compilation**

Various methods or tools exist for scripting compilation tasks. Below is an example using JavaScript and the [***zx***](https://github.com/google/zx) tool, a powerful Node shell scripting tool developed by Google. It has recently released significant improvements in version 8.x. (You can also other tool like [***bun $shell***](https://bun.sh/docs/runtime/shell), [***execa***](https://github.com/sindresorhus/execa))

```
#!/usr/bin/env zx 
import { $, chalk } from 'zx' 
 
try { 
    await `rm -rf lib` 
    await $`npx tsc -p tsconfig.lib.json --module nodenext --outDir lib/esm` 
    await $`echo '{"type": "module"}' > lib/esm/package.json` 
 
    await $`npx tsc -p tsconfig.lib.json --module commonjs --outDir lib/cjs` 
    await $`echo '{"type": "commonjs"}' > lib/cjs/package.json` 
 
    console.log(chalk.green('Compilation successful')) 
} catch (error) { 
    console.error(chalk.red('Compilation failed:'), chalk.red(error.message)) 
} 
```

This script efficiently handles compilation tasks. It use TypeScript’s compiler (**tsc**) with the appropriate configuration options to ensure compatibility with different module types.

**Key points in this script:**

- Ensure to specify output folders for both ESM and CJS builds:

```
Use - module nodenext - outDir lib/esm for ESM.
Use - module commonjs - outDir lib/cjs for CommonJS.
```

- Create nested package.json files for each build type:

```
Use $echo '{"type": "module"}' > lib/esm/package.json for ESM.
Use $echo '{"type": "commonjs"}' > lib/cjs/package.json for CommonJS.
```

These nested files allow the use of ***.js*** extensions for both CJS and ESM, preventing errors like ***“ReferenceError: require is not defined”***.

By following these steps and adapting the example to your specific project structure, you can establish effective dual-package support for your TypeScript library.

For a detailed implementation, you can find the migration code from CJS to ESM and the compilation example in [***this GitHub PR***](https://github.com/ekino/node-logger/pull/46). We made a similar transition for our library [***@ekino/node-logger,***](https://github.com/ekino/node-logger) which is a lightweight yet efficient logger that combines debug namespacing capabilities with winston levels and multi-output. Exploring this library might provide valuable insights for your projects.

**Bonus part (YAL — Yet another library)**

If you like using a quick build tool like ***esbuild*** (I really do!).

For now, ***esbuild*** doesn’t support glob pattern, so we need to use the library like [***fast-glob***](https://github.com/mrmlnc/fast-glob) to handle that part. This makes the code a bit more complex compared to using ***TSC***, but the speed boost you get from esbuild is totally worth it. Here are the scripts.

```
#!/usr/bin/env zx 
import { $, chalk } from 'zx' 
import esbuild from 'esbuild' 
import fg from 'fast-glob' 

const entryPoints = fg.sync(['src/**/*.[tj]s']) 

const buildESM = async () => { 
    try { 
        await esbuild.build({ 
            entryPoints, 
            outdir: 'lib/esm', 
            platform: 'node', 
            sourcemap: true, 
            target: 'esnext', 
            format: 'esm', 
        }) 

        await $`echo '{"type": "module"}' > lib/esm/package.json` 
        console.log(chalk.green('ESM compilation successful')) 
    } catch (error) { 
        console.error(chalk.red('ESM compilation failed:'), chalk.red(error.message)) 
    } 
} 

const buildCJS = async () => { 
    try { 
        await esbuild.build({ 
            entryPoints, 
            outdir: 'lib/cjs', 
            platform: 'node', 
            sourcemap: true, 
            target: 'esnext', 
            format: 'cjs', 
        }) 

        await $`echo '{"type": "commonjs"}' > lib/cjs/package.json` 
        console.log(chalk.green('CJS compilation successful')) 
    } catch (error) { 
        console.error(chalk.red('CJS compilation failed:'), chalk.red(error.message)) 
    } 
} 


try { 
    await $`rm -rf lib` 
    await $`npx tsc --declaration --emitDeclarationOnly --outDir lib/esm` 
    await buildESM() 
    await $`npx tsc --declaration --emitDeclarationOnly --outDir lib/cjs` 
    await buildCJS() 
    console.log(chalk.green('Overall compilation successful')) 
} catch (error) { 
    console.error(chalk.red('Overall compilation failed:'), chalk.red(error.message)) 
} 
```

**Thanks**

Special thanks to all the members at [@ekino](https://medium.com/ekino-france) who contributed to and reviewed this article.

**Stay connected**

Stay updated with our latest insights and stories from Ekino by following our page [here](https://medium.com/ekino-france).

---

[Supporting Dual Package for CJS and ESM in TypeScript Library](https://medium.com/ekino-france/supporting-dual-package-for-cjs-and-esm-in-typescript-library-b5feabac1357) was originally published in [ekino-france](https://medium.com/ekino-france) on Medium, where people are continuing the conversation by highlighting and responding to this story.
