-
Notifications
You must be signed in to change notification settings - Fork 25
Migrate Your Plugin to ESM
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.
There are two main benefits to using ESM.
- Dependencies imports are more efficient, and thus command executions are faster.
- You can seamlessly continue to use the latest versions of dependencies that have migrated to ESM. For instance,
chalk
,got
, andinquirer
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.
The type
key of your plugin's package.json
file tells Node.js what kind 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:
{
"name": "@salesforce/plugin-org",
"description": "Commands to interact with Salesforce orgs",
<lots of stuff>
"type": "module"
}
Your plugin's tsconfig.json
file should 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, which is what we recommended, and is what Salesforce CLI itself uses. See plugin-org for an example.
Likewise, test/tsconfig.json
should now extend @salesforce/dev-config/tsconfig-test-esm
or @salesforce/dev-config/tsconfig-test-strict-esm
. Here's how plugin-org does it.
Replace the contents of your plugin's bin
directory with these files.
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, you must now explicitly specify index.js
.
Before:
import { utils } from '../utils'
After:
import { utils } from '../utils/index.js'
ESM doesn't support using require()
, so you must replace it with import
. For example:
Before:
const fs = require('node:fs');
After:
import fs from 'node:fs';
If you absolutely need require
, because maybe you need require.resolve
, then you can create it like this:
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
For example:
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';
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:
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)
.
If you used the dev generate plugin
command to generate your plugin, then the src/index.ts
file probably looks like this:
export = {}
Update the file to look like this:
export default {}
Rename config files such as commitlint.config.js
to commitlint.config.cjs
. Alternatively, migrate the files to ESM.
Add the following to your .mocharc.json
file so that mocha can run ESM:
{
"node-option": ["loader=ts-node/esm"]
}
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 you must ensure that the source file and test file are importing the fs
module in the exact 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
// 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')
© Copyright 2024 Salesforce.com, inc. All rights reserved. Various trademarks held by their respective owners.
- Quick Intro to Developing sf Plugins
- Get Started: Create Your First Plugin
- Design Guidelines
- Code Your Plugin
- Debug Your Plugin
- Write Useful Messages
- Test Your Plugin
- Maintain Your Plugin
- Integrate Your Plugin With the Doctor Command
- Migrate Plugins Built for sfdx
- Conceptual Overview of Salesforce CLI