Hybrid ESM/CJS Node Packages using TypeScript (Take 2)
A colleague of mine at Prisma (👨 Tyler) tipped me off to a simpler approach to creating hybrid CJS/ESM packages. His insight was how Node.js respects the presence and contents of package.json
files…
You can find a minimal functional example applying the pattern below in my TypeScript library template repository.
-
First have two
tsconfig
files, one for CJS and one for ESM. Make both inherit from a basetsconfig.json
that contains non emit configuration (e.g.strict
mode):// tsconfig.cjs.json { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "build/cjs", "module": "commonjs", "moduleResolution": "node", "rootDir": "src" }, "include": ["src"], "exclude": ["**/*.spec.*"] }
// tsconfig.esm.json { "extends": "./tsconfig.json", "compilerOptions": { "outDir": "build/esm", "rootDir": "src" }, "include": ["src"], "exclude": ["**/*.spec.*"] }
-
Have a
package.json
that looks something like the following. Importantly notice that the build forCJS
includes an emit of apackage.json
file containing thetype: "commonjs"
entry. This is small tweak will cause Node.js to view all JavaScript files within that emit directory to be CJS, easy! Thanks Tyler 🙌 ! In the following I also believe that the explicittypes
fields are optional when their names match the name of the entrypoint. Furthermore only TS 4.7 using its new ESM mode or TS 5.0 using its new flag can read from thoseexports
field. Thus it might be a good idea for backwards compatibility (at least until TS 5+ gains widespread adoption… say 6 months??) to include atypes
field pointing at one of the*.d.ts
entrypoints. Note thattypes
does not support multi-entrypoint packages and thus is to be ditched as soon as practical. In my opinion a few months post TS 5.0 will be good enough.{ "name": "foobar", "type": "module", "files": ["build"], "exports": { ".": { "require": { "types": "./build/cjs/index.d.ts", "default": "./build/cjs/index.js" }, "import": { "types": "./build/esm/index.d.ts", "default": "./build/esm/index.js" } } }, "scripts": { "build": "pnpm build:cjs && pnpm build:esm", "build:cjs": "pnpm tsc --project tsconfig.cjs.json && echo '{\"type\":\"commonjs\"}' > build/cjs/package.json", "build:esm": "pnpm tsc --project tsconfig.esm.json" } }