Thursday, December 8, 2022

ESM-migration

After some tests with Deno land development and some import with esm.sh, I think it's worth migrating my commonJS project to ESM.

But replacing commonJS with ESM, like sindresorhus did, will bring breaking changes and devastation around my existing code base.

p-map case

Take p-map and look at the version usage; Most people use the old version. p-map is an ESM-only package, so most users are currently stuck with older versions.

That is mostly the case for all sindresorhus's packages.

Dual stack

The best way is to publish dual staked NodeJS package. For that, you need some build chain to refactor and package.json upgrade.

tsconfig changes

By having 100% of my project written in typescript, I can apply the same change on all of them:

Create a new tsconfig-cjs.json

{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "module": "commonjs",
    "declaration": false,
    "declarationMap": false,
    "outDir": "dist/cjs"
  }
}

then update the main tsconfig.json to generate ESM modules, so in tsconfig.json change:

{
  "compilerOptions": {
    "outDir": "dist/esm",
    "moduleResolution": "node",
    "module": "ES2020",
    "declaration": true
  }
}

I also recommend using an include statement in your tsconfig instead of exclude, you only need to specify the entry point in the include, not all your source files.

Note: we only want to generate the declaration once in the ESM dest.

package.json changes

Do no define any "type": "module" or you cjs code will not be acceissible.

Replace your main entries:

from

"main": "dist/index.js",
"typings": "dist/index.d.ts",

to

"main": "dist/cjs/index.js",
"module": "./dist/esm/index.js",
"typings": "dist/esm/index.d.ts",
"exports": {
  ".": {
    "types": "./dist/esm/index.d.ts", // "types" - can be used by typing systems to resolve the typing file for the given export. This condition should always be included first. see https://nodejs.org/api/packages.html#community-conditions-definitions
    "require": "./dist/cjs/index.js",
    "import": "./dist/esm/index.js",
    "default": "./dist/cjs/index.js"
  }
},
"files": [ "dist" ]

Update the build script:

{
  "scripts": {
    "prepublishOnly": "rimraf dist",
    "build": "tsc --pretty --project . && tsc --pretty --project tsconfig-cjs.json",
    "build": "tsc --build tsconfig.json --pretty", // new Version
    "prepare": "npm run build"
  }
}

Now your package is compatible with most nodeJS projects.

extra verifications step

Before any commit or publish, double check:

  • your clean script
  • your package.json files entry must include newly generated files

First, publish using --dry-run and --tag to avoid changing the latest tag turning test.

ESM migration changes

In ESM all inport must include an file extention, import dep from './file' will become import dep from './file.js' to fix vs code auto import add in your .vscode/settings.json:

{
  "javascript.preferences.importModuleSpecifierEnding": "js",
  "typescript.preferences.importModuleSpecifierEnding": "js"
}

External migration blog post

Note for myself