This monorepo is designed to test the development and distribution of dual-packages that can be consumed by both CommonJS and ESM (ECMAScript Module) Node.js applications. It is managed using pnpm. The structure of the monorepo consists of two main folders: apps
and packages
.
The primary purpose of this monorepo is to enable the development of packages compatible with both CommonJS and ESM, allowing them to be used in different Node.js environments seamlessly. Achieving this compatibility requires specific configurations in the package.json
file of each package, along with leveraging TypeScript's latest features.
In your package's package.json
file, you can use the exports
field to specify the entry points for your package, enabling dual-package support. Here's an example of how to set up the exports
field for a package:
{
"type": "module", // Indicates that the packages is written as ESM
"exports": {
".": {
"import": "./dist/index.js", // ESM entry point
"require": "./dist/index.cjs" // CommonJS entry point
}
},
// Other package.json configuration...
}
In this example:
"type": "module"
indicates that the package is written as ECMAScript Modules (ESM). This means the package source (.ts
files) will be emitted as .js files with ESM syntax. Every dual package has some kind of build process that emits CommonJS, too.- The
"exports"
field specifies the entry points for both ESM and CommonJS consumers."import"
points to the ESM entry point."require"
points to the CommonJS entry point.
To ensure TypeScript understands and resolves the ESM and CommonJS entry points correctly, you can use TypeScript's module
and moduleResolution
options in your tsconfig.json
:
{
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "NodeNext",
// Other TypeScript options...
}
}
With "module": "NodeNext"
and "moduleResolution": "NodeNext"
TypeScript will correctly handle imports and resolve ESM and CommonJS modules based on the specified entry points in the exports
field of the package's package.json
.
Before you begin, ensure you have the following prerequisites installed:
-
Clone this repository to your local machine:
git clone https://github.com/ducktors/dual-package-reference.git
-
Navigate to the root directory of the monorepo:
cd dual-package-reference
-
Install the dependencies for the entire monorepo using pnpm:
pnpm install
First, from the repo's root directory, build all the packages and applications calling:
$ pnpm build
.
After everything is built, you can run all applications from the root folder using:
$ pnpm start
This monorepo installs arethetypeswrong as a dependency. From the monorepo root folder, you can check the compatibility of every package calling:
$ pnpm attw
This repository explores all possible source and target combinations using tsc and tsup to build the dual-packages. We also use "composite": true
in the tsconfig.json
files to enable the references
field in the tsconfig.json
files of the applications. This allows us to consume the dual-packages as part of the monorepo and have a faster and better experience within the IDE.
The following table shows the possible combinations of source and targets:
name | compiler | package.json type |
---|---|---|
dual-package-tsc-cjs | tsc | CommonJS |
dual-package-tsc-esm | tsc | ESM |
dual-package-tsup-cjs | tsup | CommonJS |
dual-package-tsup-esm | tsup | ESM |
The packages that use tsc as the compiler need two different tsconfig.json
files to emit both formats properly. In dual-package-tsc-cjs
package, this additional configuration file is called tsconfig.esm.json
since the default compilation emits ESM thanks to the "type": "module"
property in the package.json
file. On the other hand, dual-package-tsc-esm
package needs a tsconfig.cjs.json
file to emit CommonJS.
As of this exploration's date, tsup cannot emit declaration maps. This prevents the package that uses tsup from having a correct "go-to definition" behavior in monorepos. In fact, when an ESM ("type": "module"
in package.json) first package is consumed inside CommonJS application ("type": "commonjs"
in package.json), the IDE will jump to the *.d.cts
file. The same goes if a CommonJS package is consumed inside an ESM application. It works only when the type of the application and the package match.
The app folder shows you how to consume a dual-pacakge.
To do it properly, you must add a reference inside the application's tsconfig.json
pointing to the dual-package's tsconfig.json
file. This will allow the IDE to resolve the imports and provide a better experience. Moreover, after running the application, you can check that based on the application type ("type": "module"
or "type": "commonjs"
), the correct entry point is used. The opposite compilation target is used when the application's and package's types do not match.
MIT