Skip to content

Migrate Plugins Built for sfdx

Willie Ruemmele edited this page Apr 18, 2024 · 12 revisions

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

If you haven't already, update @oclif and the @salesforce/core library to their current versions.

  1. Update to v3 of @oclif/core by following this document.
  2. Update to v7 of @salesforce/core by following this document. There are documents detailing every major change, and upgrade tips at the root of the repository, which might be helpful if you're upgrading from an old version.

Tools and Tips to Help You Migrate Your Plugin

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

When 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 setup instructions for more details.

Be aware that some of these eslint rules automatically change some of your code while other rules provide suggestions where you can choose 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. If you haven't already, install plugin-dev:

    sf plugins install dev
  2. 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:

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

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

  3. Edit the resulting Markdown file to ensure your messages look good. See the next section for a tip to reduce maintenance, and then Write Useful Messages for general information about writing messages.

  4. 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.

  5. Remember to delete the old JSON files when 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. If your plugin supports both sfdx and sf, use these placeholders in message files rather than hardcoding the command and CLI executable name. This way you won't need to rewrite the messages when you change your command structure or name.

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

# examples

- Delete a sandbox with alias my-sandbox:

  <%= 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 Your Plugin for details. If you need to maintain backward compatibility, you have some options.

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

export class LimitsApiDisplayCommand extends SfCommand<ApiLimits> {
  ...
  // this command is now `limits api display` but was previously 'force:limits:api:display'` so an alias keeps that old name operable
  public static readonly aliases = ['force:limits:api:display', 'org:list:limits'];

  // when someone uses the old name, display a warning encouraging them to use the new name.
  // without this property, the old name works but no warning occurs.
  public static deprecateAliases = true;

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

    sobject: Flags.string({
      char: 's',
      required: true,
      summary: messages.getMessage('flags.sobject'),
      // the flag's previous name could be simplified to a single word
      aliases: ['sobjecttype'],
      // display a warning when someone uses the previous flag name
      deprecateAliases: true,
    }),
    // sf uses the more readable hyphenated name
    'use-tooling-api': Flags.boolean({
      char: 't',
      summary: messages.getMessage('flags.useToolingApi'),
      aliases: ['usetoolingapi'],
      deprecateAliases: true,
    }),
    // this flag was renamed and its short character was changed.  both -f and -p work now, but -p provides a warning when used
    'file': Flags.boolean({
      char: 'f',
      summary: messages.getMessage('flags.useToolingApi'),
      aliases: ['pathtofile', 'p'],
      deprecateAliases: true,
    }),

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

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

Key Differences Between sfdxCommand and sfCommand

As described here, the eslint plugin handles most of the work of getting you to sfCommand. But there are some differences you should be aware of.

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'll need to 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:

// SfdxCommand
myHelper(this.ux); 

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

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

No Automatic apiVersion

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

public static flags = {
  'target-org': requiredOrgFlagWithDeprecations,
  'api-version': orgApiVersionFlagWithDeprecations,
}

// when you get a Connection, pass the api-version flag to it
// that way, if the user specified an api version, the Connection is set
const conn = flags['target-org'].getConnection([flags.api-version]);

Test Migration

After you run yarn remove @salesforce/command you might 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/connection and calls a command's run method.

Testing Aliases

It's helpful to leave existing tests to verify that you've properly aliased all commands and flags to avoid breaking changes.

Clone this wiki locally