Skip to content

Migrate Your Plugin to ESM

Juliet Shackell edited this page Dec 6, 2023 · 8 revisions

Node.js supports two ways of packaging JavaScript and TypeScript code: CommonJS modules and ECMAScript modules (or ESM). If you generated your Salesforce CLI plugin using sf dev generate before November 2023, your plugin is written with CommonJS modules. After November 2023, sf dev generate generates plugins using ESM.

Salesforce will continue to support CommonJS-based plugins in sf. But because the Node.js ecosystem is generally moving towards ESM, we recommend that you migrate your plugin to ESM so that 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 are not 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 are 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.

Migrate to ESM

Add type: "module" to package.json

This tells node what kind of modules your plugin is using. Without this, node will assume your plugin is CommonJS.

Update tsconfig.json

Your tsconfig.json should now extend either @salesforce/dev-config/tsconfig-esm or @salesforce/dev-config/tsconfig-strict-esm

Likewise, test/tsconfig.json should now extend @salesforce/dev-config/tsconfig-test-esm or @salesforce/dev-config/tsconfig-test-strict-esm

Update bin scripts

Replace the contents of the bin directory with these: https://github.com/oclif/hello-world-esm/tree/main/bin

Add .js extension to all file imports

In ESM all local imports must have the .js extension. For example:

before:

import { utils } from '../utils'

after:

import { utils } from '../utils.js'

If you're importing from an index file, then you'll have to include index too:

before:

import { utils } from '../utils'

after:

import { utils } from '../utils/index.js'

Replace require with import

ESM does not have a require so you'll need to replace those with import. If you absolutely need require (perhaps you need require.resolve) then you can create it like this:

import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);

No more import *

Replace import * with import. For instance:

before:

import * as fs from 'node:fs'
import * as chalk from 'chalk';
import * as utils from '../utils';

after:

import fs from 'node:fs'
import chalk from 'chalk';
import utils from '../utils.js';

Replace __dirname and __filename

This is most often found wherever you're instantiating Messages. This is how it should now be done:

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');

__filename and simply be replaced with fileURLToPath(import.meta.url)

Update src/index.ts

If you used the generator then you're src/index.ts probably looks like this:

export = {}

You just need to change it to the following:

export default {}

Use .cjs for .js config files.

Config files like commitlint.config.js should be renamed to commitlint.config.cjs or migrated to ESM.

Tests

Update .mocharc.json

Add the following so that mocha can run ESM,

{
  "node-option": ["loader=ts-node/esm"]
}

Make imports stubbable

The sinon library can only stub ESM if the imports in the source code must match the imports used in the tests.

For instance, if you would like to stub writeFileSync from fs, then you will to ensure that the source file and test file are importing the fs module in the exact same way.

The first example is not stubbable because the source file uses a deconstructed import but the test file does not. The second example fixes this by using the same import fs from 'node:fs' in both files.

❌ Not stubbable

// 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

// 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')
Clone this wiki locally