Skip to content

Build CommonJS and ESM from one package

This guide builds esm/ and cjs/ from the same src/, and publishes both through exports. It uses buddy ts build, which runs tsc and marks cjs/ as CommonJS with package.cjs.json.

  1. Install the packages:

    Terminal window
    pnpm add -D typescript repobuddy @repobuddy/typescript
  2. Enable the plugin. Add a repobuddy property to package.json, or create a .repobuddy.json with the same content:

    {
    "repobuddy": {
    "plugins": ["@repobuddy/typescript"]
    }
    }
  3. Set "type": "module" in package.json, so Node.js reads esm/*.js as ESM.

  4. Create the base tsconfig.json. It holds the ESM settings:

    {
    "extends": "@repobuddy/typescript/tsconfig/monorepo",
    "compilerOptions": {
    "outDir": "esm",
    "rootDir": "src"
    },
    "include": ["src"]
    }
  5. Create tsconfig.esm.json. buddy ts build esm reads this file name:

    {
    "extends": "./tsconfig.json"
    }
  6. Create tsconfig.cjs.json. List modules/buddy-commonjs after the base config so its module: CommonJS wins. Set outDir to cjs, because the command copies package.json into a folder named after the build type:

    {
    "extends": ["./tsconfig.json", "@repobuddy/typescript/tsconfig/modules/buddy-commonjs"],
    "compilerOptions": {
    "outDir": "cjs"
    }
    }
  7. Point exports at both builds and add a build script:

    {
    "exports": {
    ".": {
    "import": {
    "types": "./esm/index.d.ts",
    "default": "./esm/index.js"
    },
    "require": {
    "types": "./cjs/index.d.ts",
    "default": "./cjs/index.js"
    }
    }
    },
    "files": ["cjs", "esm"],
    "scripts": {
    "build": "buddy ts build esm && buddy ts build cjs"
    }
    }
  8. Run the build:

    Terminal window
    pnpm build
    building esm...
    build esm done
    building cjs...
    copying package.json...
    build cjs done
package.json
{
"name": "my-lib",
"version": "1.0.0",
"type": "module",
"exports": {
".": {
"import": {
"types": "./esm/index.d.ts",
"default": "./esm/index.js"
},
"require": {
"types": "./cjs/index.d.ts",
"default": "./cjs/index.js"
}
}
},
"files": ["cjs", "esm"],
"scripts": {
"build": "buddy ts build esm && buddy ts build cjs"
},
"repobuddy": {
"plugins": ["@repobuddy/typescript"]
}
}

tsconfig.json, tsconfig.esm.json, and tsconfig.cjs.json are as in steps 4 to 6.

  1. Check that cjs/package.json exists and contains { "type": "commonjs" }.

  2. From another project that installs the package, load it both ways:

    Terminal window
    node -e "console.log(require('my-lib'))"
    node --input-type=module -e "import * as m from 'my-lib'; console.log(m)"

    Both print the package’s exports.

  • Helpers from tslib: add tsconfig.tslib.json that extends tsconfig.cjs.json and sets "importHelpers": true and "outDir": "tslib". Run buddy ts build tslib. The package needs tslib as a dependency. testcases/build-ts in this repository does this.
  • Another CommonJS build tool: if a bundler writes cjs/, skip buddy ts build cjs and run buddy ts cpj cjs . after it. This repository’s packages/jest bundles cjs/index.js with esbuild, emits only declarations with tsc -p tsconfig.cjs.json (emitDeclarationOnly), and then copies its own package.cjs.json to cjs/package.json.