Single Source Base
Write your code in ES modules using import and export.
1 | import Shape from './Shape.js' |
Building
Build the source twice, once for ESM and once for CommonJS.
We use Typescript as our transpiler, and author in ES6/ES-Next or Typescript. Alternatively, Babel would work fine for ES6.
Javascript files should have a .js extension, Typescript files will have a .ts extension.
Here is our package.json build script:
package.json:
1 | { |
The tsconfig.json is setup to build for ESM and tsconfig-cjs.json builds for CommonJS.
To avoid duplication of settings, we define a shared tsconfig-base.json that contains shared build settings used for both ESM and CommonJS builds.
The default tsconfig.json is for ESM and builds using "esnext". You can change this to "es2015" or any preset you desire.
tsconfig.json:
1 | { |
tsconfig-cjs.json:
1 | { |
Here is our tsconfig-base.json for ES6 code with all shared settings:
tsconfig-base.json:
1 | { |
For Web browsers packages
Note: If you are building packages for web browsers, you might need to generate UMD module as well. Unfortunately, TypeScript’s UMD output does not support creating a browser global. You would better use an external bundler such as rollup.js.
1 | npm install rollup rollup-plugin-typescript2 --save-dev |
rollup.config.js
1 | import typescript from 'rollup-plugin-typescript2'; |
In this case, also you need to change package.json:
1 | "scripts": { |
Per ESM/CJS package.json
The last step of the build is a simple fixup script that creates per-distribution package.json files. These package.json files define the default package type for the .dist/* sub-directories.
fixup:
1 | cat >dist/cjs/package.json <<!EOF |
Alternatively, you can put this logic into a .js file, such as fixup.js:
1 | import fs from 'fs'; |
Then change the scripts part of your package.json:
1 | "scripts": { |
Package.json
Our package.json does not have a type property. Rather, we push that down to the package.json files under the ./dist/* sub-directories.
We define an exports map which defines the entry points for the package: one for ESM and one for CJS.
Here is a segment of our package.json:
package.json:
1 | "main": "dist/cjs/index.js", |
Summary
With the above strategy, modules can be consumed using import or require. And you can use a single code base that uses modern ES6 or Typescript. Users of your ESM distribution get the benefit of increased performance and easier debugging.