---
schema_version: "1"
id: 912a3ad450c56d820fdf488a9e83458fd922a61fc9799c39ae01a9e1d9176f7a
title: Migrate Your Plugin to ESM
collection: developer
version: current
locale: en-us
portal_url: https://docs.salesforce.com/developer/current/en-us/salesforcedx/sfcliplugin/guides/maintain/maintain-migrate-esm.md
canonical_url: https://developer.salesforce.com/docs/platform/salesforce-cli-plugin/guide/maintain-migrate-esm.html
source_site: developer.salesforce.com
content_type: documentation
content_hash: b892b01907bd031cc9f0cdf838284611641c9358f44522e2062ac0daaf7e8fee
products:
  - Salesforce CLI Plugin Developer Guide
  - salesforcedx
---
# Migrate Your Plugin to ESM

Node.js supports two ways of packaging JavaScript and TypeScript code: [CommonJS modules](https://nodejs.org/docs/latest/api/modules.html) and [ECMAScript modules](https://nodejs.org/api/esm.html) (or _ESM_). If you generated your Salesforce CLI plugin using `sf dev generate` before November 2023, the command generates plugins that use CommonJS modules. After November 2023, `sf dev generate` generates plugins that use ESM.

Salesforce continues to support CommonJS-based plugins in `sf`. But because the Node.js ecosystem is moving towards ESM, we recommend that you migrate your plugin to ESM. This way you can take advantage of the latest and greatest updates.

## Two Benefits and a Caveat

There are two main benefits to using ESM.

1. Dependencies imports are more efficient, and thus command executions are faster.
2. You can seamlessly continue to use the latest versions of dependencies that have migrated to ESM. For instance, `chalk`, `got`, and `inquirer` have all migrated to ESM. While you can continue to use these dependencies in CommonJS, you must change your imports of them to dynamic imports (for example, `await import('chalk')`), but they can't be top-level imports. Migrating your plugin to ESM allows you to stay on the latest without the inconvenience of dynamic imports.

But here's the caveat: Currently, linked ESM plugins aren't auto compiled at runtime. This is a limitation with the current state of ESM and Node.js. We hope to support this again in the future. But in the meantime, you must either run `yarn compile` on your plugin before linking it. Or, if you're actively developing a plugin, open a separate terminal to your plugin directory and run `yarn tsc -w` to immediately compile your changes as you save them.

## Steps to Migrate to ESM

Follow these steps to migrate your plugin to ESM.

### Add `type: "module"` to `package.json`

The `type` key of your plugin's `package.json` file tells Node.js what type of modules your plugin is using. Without this key, Node.js assumes your plugin is CommonJS. Here's a pared-down example from [plugin-org](https://github.com/salesforcecli/plugin-org/blob/main/package.json):

```json
{
  "name": "@salesforce/plugin-org",
  "description": "Commands to interact with Salesforce orgs",
  <lots of stuff>
  "type": "module"
}
```

### Update the `extends:` Key in `tsconfig.json`

Your plugin's `tsconfig.json` file must now extend either `@salesforce/dev-config/tsconfig-esm` or `@salesforce/dev-config/tsconfig-strict-esm`. The `strict` variant adds `strict: true` to the [TSConfig file](https://www.typescriptlang.org/tsconfig#strict), which is what we recommended, and is what Salesforce CLI itself uses. See [plugin-org](https://github.com/salesforcecli/plugin-org/blob/main/tsconfig.json) for an example.

Likewise, `test/tsconfig.json` must now extend `@salesforce/dev-config/tsconfig-test-esm` or `@salesforce/dev-config/tsconfig-test-strict-esm`. Here's how [plugin-org](https://github.com/salesforcecli/plugin-org/blob/main/test/tsconfig.json) does it.

### Update the `bin` Scripts

Replace the contents of your plugin's `bin` directory with [these](https://github.com/oclif/hello-world-esm/tree/main/bin) files.

### Add the `.js` Extension to All File Imports

In ESM, all local imports must have the `.js` extension. For example:

**Before:**

```typescript
import { utils } from "../utils";
```

**After:**

```typescript
import { utils } from "../utils.js";
```

If you're importing from an `index` file, you must now explicitly specify `index.js`.

**Before:**

```typescript
import { utils } from "../utils";
```

**After:**

```typescript
import { utils } from "../utils/index.js";
```

### Replace `require` With `import`

ESM doesn't support using `require()`, so you must replace it with `import`. For example:

**Before:**

```typescript
const fs = require("node:fs");
```

**After:**

```typescript
import fs from "node:fs";
```

If you absolutely need `require`, because maybe you need `require.resolve`, then you can create it like this:

```typescript
import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
```

### Replace `import * as` With Just `import`

For example:

**Before:**

```typescript
import * as fs from "node:fs";
import * as chalk from "chalk";
import * as utils from "../utils";
```

**After:**

```typescript
import fs from "node:fs";
import chalk from "chalk";
import utils from "../utils.js";
```

### Replace `__dirname` and `__filename` With `fileUrlToPath()`

ESM doesn't support the `__dirname` and `__filename` variables. You most likely use them when instantiating `Messages.` Use the `fileUrlToPath(import.meta.url)` method instead. Here's a `__dirname` example:

```typescript
import { dirname } from "node:path";
import { fileURLToPath } from "node:url";

Messages.importMessagesDirectory(dirname(fileURLToPath(import.meta.url)));
const messages = Messages.loadMessages("my-plugin", "my-command");
```

Replace `__filename` with just `fileURLToPath(import.meta.url)`.

### Update `src/index.ts`

If you used the `dev generate plugin` command to generate your plugin, then the `src/index.ts` file probably looks like this:

```typescript
export = {};
```

Update the file to look like this:

```typescript
export default {};
```

### Rename `.js` Config Files to `.cjs`

Rename config files such as `commitlint.config.js` to `commitlint.config.cjs`. Alternatively, migrate the files to ESM.

### Update Your Tests

#### Update `.mocharc.json`

Add this to your `.mocharc.json` file so that mocha can run ESM:

```json
{
  "node-option": ["loader=ts-node/esm"]
}
```

#### Make Imports Stubbable

The `sinon` library can stub ESM only if **the imports in the source code match the imports used in the tests**.

For example, if you want to stub `writeFileSync` from `fs`, then make sure that the source file and test file import the `fs` module in the same way.

The first example isn't stubbable because the source file uses a deconstructed `import`, but the test file doesn't. The second example fixes this by using the same `import fs from 'node:fs'` in both files.

❌ Not stubbable

```typescript
// src/util.ts
import { writeFileSync } from "node:fs";

export function doStuff() {
  return writeFileSync("stuff.txt", "hello world");
}

// test/util.test.ts
import fs from "node:fs";
stub(fs, "writeFileSync");
```

✅ Stubbable

```typescript
// src/util.ts
import fs from "node:fs";

export function doStuff() {
  return fs.writeFileSync("stuff.txt", "hello world");
}

// test/util.test.ts
import fs from "node:fs";
stub(fs, "writeFileSync");
```
