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
tsconfigfiles, one for CJS and one for ESM. Make both inherit from a basetsconfig.jsonthat contains non emit configuration (e.g.strictmode):// 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.jsonthat looks something like the following. Importantly notice that the build forCJSincludes an emit of apackage.jsonfile 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 explicittypesfields 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 thoseexportsfield. Thus it might be a good idea for backwards compatibility (at least until TS 5+ gains widespread adoption... say 6 months??) to include atypesfield pointing at one of the*.d.tsentrypoints. Note thattypesdoes 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" } }