On this post I'll describe you how you can easily add support for both CJS (CommonJS) and ESM (ECMAScript Modules) when authoring your own NPM packages.
Now that TypeScript is the go-to choice for most NPM package authors, it has never been easier to support either both CJS and ESM as "build targets".
The problem
The standard TypeScript compiler (tsc
) is still a bit limited when it comes to ESM output. If you use .ts
for your source-code files, it simply can't output .mjs
files for you. Although there are workarounds for this limitation, it often becomes too much effort, specially if you are under a monorepo with more than one NPM package to create releases from.
The solution
Use esbuild
to generate both .js
and .mjs
files, and tsc
only for the declaration files (.d.ts
).
You can use the same output directory for all of them.
npm install --save-dev esbuild
Generating the CommonJS (CJS) output:
npx esbuild --outdir=build --platform=node --format=cjs src/*.ts
Generating the ES Modules (ESM) output:
npx esbuild --out-extension:.js=.mjs --outdir=build --platform=node --format=esm src/*.ts
Generating the TypeScript declaration files:
npx tsc
Alternatively, you can do these 3 steps through both esbuild
and typescript
APIs. See the full example using esbuild
and typescript
APIs.
Finally, for the package.json
, you can combine all the 3 outputs:
"main": "./build/index.js",
"module": "./build/index.mjs",
"typings": "./build/index.d.ts",
Hope this helps!
Top comments (3)
This post fails to argue why you want us package publishers to offer both formats and double our workload.
I only publish ESM, which works fine in Node 16, Node 18, and Webpack 5.
Sorry should've argued on the why, but my point of view is that Node.js/JavaScript has users from all types of backgrounds, and more often than you think a "legacy" (or not too legacy) project that heavily uses CJS desperately needs a module that is only available as ESM.
There are still loads of content and tutorials teaching beginners how to use CJS, and they'll stick with it without questioning the existance of ESM, and there you have another project starting as CJS.
You can call ESM module from CJS module via
import()
expression. No desperation needed.