Migrate Plugins Built for sfdx to sf

As of April 2023, the sfdx version of Salesforce CLI is deprecated and Salesforce no longer supports it. The latest and supported Salesforce CLI version is sf. However, many customers created their own plugins using the sfdx framework and libraries. If you’re such a customer, read on.

You can install and use plugins originally built for sfdx in sf, although the general look-and-feel is different. We therefore recommend that you explicitly migrate your sfdx plugin to sf so its commands look and behave the same as other sf commands. You can also then take advantage of the many improvements we’ve made to the code that supports sf plugins.

Prerequisites 

Update @oclif and the @salesforce/core library to the latest versions.

  1. Update to v3 (or latest) of @oclif/core as described by this document.
  2. Update to v7 (or latest) of @salesforce/core as described this document. The root of the repository contains other MIGRATING_VX_VY.MD documents that detail every major change and upgrade tips.

Tools and Tips 

These sections provide useful information for migrating your plugins from sfdx to sf.

ESLint Configuration 

Salesforce provides a set of eslint rules for Salesforce CLI plugins.

While migrating an sfdx plugin to sf, we recommend you use a set of additional rules that do most of the work for you. See the set up instructions for more details.

Some of these eslint rules automatically change some of your code while other rules provide suggestions where you can select which fix to apply.

Migrate Messages to Markdown 

Although v3+ of @salesforce/core works with your existing JSON message files, we recommend that you migrate them to Markdown. Message files contain the --help content and error messages.

  1. From the top-level directory of your plugin, run the dev convert messages command to convert an existing JSON file to a first-draft Markdown file to get you started. For example:

    1sf dev convert messages --file-name messages/mycommand.json

    In this case, the command generates the Markdown file messages/mycommand.md.

  2. Edit the generated Markdown file to make sure your messages look good. See the next section for information about reducing maintenance and Write Useful Messages for general information about writing messages.

  3. Run sf dev audit messages to find missing or unused messages, then update your code to add or remove references to the messages as needed.

  4. Delete the old JSON files after you’re finished.

Dynamic Help and Error Messages 

Messages in v3+ of @salesforce/core support dynamic references to the command and Salesforce CLI binary names. Specifically, the placeholder <%= config.bin %> refers to the binary name (sfdx or sf) and <%= command.id %> refers to the command itself.

If your plugin supports both sfdx and sf, use these placeholders in message files rather than hard coding the command and CLI executable name. As a result, you aren’t required to rewrite the messages after you change your command structure or name.

Here’s an example from the Markdown message file for the org delete sandbox command:

1## examples
2
3- Delete a sandbox with alias my-sandbox:
4
5  <%= config.bin %> <%= command.id %> --target-org=my-sandbox

Backward Compatibility 

We encourage you to follow sf’s styles in your command and flag names. See Design Guidelines for details. If you must maintain backward compatibility, you have some options.

If you rename a command, you can alias it back to its original name.

1export class LimitsApiDisplayCommand extends SfCommand<ApiLimits> {
2  ...
3  // this command is now `limits api display` but was previously 'force:limits:api:display'` so an alias keeps that old name operable
4  public static readonly aliases = ['force:limits:api:display', 'org:list:limits'];
5
6  // when someone uses the old name, display a warning encouraging them to use the new name.
7  // without this property, the old name works but no warning occurs.
8  public static deprecateAliases = true;

Similarly, if you rename a flag, you can alias it back to its previous name.

1sobject: Flags.string({
2      char: 's',
3      required: true,
4      summary: messages.getMessage('flags.sobject'),
5      // the flag's previous name could be simplified to a single word
6      aliases: ['sobjecttype'],
7      // display a warning when someone uses the previous flag name
8      deprecateAliases: true,
9    }),
10    // sf uses the more readable hyphenated name
11    'use-tooling-api': Flags.boolean({
12      char: 't',
13      summary: messages.getMessage('flags.useToolingApi'),
14      aliases: ['usetoolingapi'],
15      deprecateAliases: true,
16    }),
17    // this flag was renamed and its short character was changed.  both -f and -p work now, but -p provides a warning when used
18    'file': Flags.boolean({
19      char: 'f',
20      summary: messages.getMessage('flags.useToolingApi'),
21      aliases: ['pathtofile', 'p'],
22      deprecateAliases: true,
23    }),

We provide some helpers for compatibility within @salesforce/sf-plugins-core. We’ve marked them as deprecated because you use these helpers only for migrating an existing sfdx plugin, not for new plugin development.

1public static flags = {
2    // allow but warn on --targetusername and -u
3    'target-org': requiredOrgFlagWithDeprecations,
4    // allow but warn on --apiversion
5    'api-version': orgApiVersionFlagWithDeprecations,
6    // loglevel is a no-op, but this flag is added to avoid breaking scripts and warn users who are using it
7    loglevel,
8  };

Key Differences Between "sfdxCommand" and "sfCommand" 

As described in this section, the eslint plugin handles most of the work of getting you to sfCommand. But there are some differences.

Properties That Don't Exist 

Some properties like this.logger don’t automatically exist. You can create the property by importing Logger from sfdx-core. Other properties like requiresUsername don’t do anything and are removed by the linter rules. You must add a flag for the org.

this.ux 

SfCommand has a private ux property, which means you can’t pass it to helper functions. Instead of this code:

1// SfdxCommand
2myHelper(this.ux);

Use this code to construct a new Ux instance and pass it to your help:

1import { Ux, Flags, SfCommand } from '@salesforce/sf-plugins-core'
2...
3// myHelper can call any `Ux` method and terminal output is suppressed if json enabled
4myHelper(new Ux({jsonEnabled: this.jsonEnabled()}))

No Automatic apiVersion 

SfdxCommand’s requires|supports(devhub)username properties created a flag for both the username an apiversion flag. If your command must support an apiversion, or you want to keep it in place for compatibility, do this:

1public static flags = {
2  'target-org': requiredOrgFlagWithDeprecations,
3  'api-version': orgApiVersionFlagWithDeprecations,
4}
5
6// when you get a Connection, pass the api-version flag to it
7// that way, if the user specified an api version, the Connection is set
8const conn = flags['target-org'].getConnection([flags.api-version]);

Test the Migration 

After you run yarn remove @salesforce/command you sometimes see that you were also using its test library, based on fancy-test.

sfdx-core v3+ includes better tools for mocking auths/orgs/projects/users/connections. See its docs.

This example uses TestSetup to mock an org and a connection and calls a command’s run method.

Testing Aliases 

To avoid breaking changes, we recommend that you leave existing tests which verify that you’ve correctly aliased all commands and flags.