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.