Skip to content

Get Started And Create Your First Plug In

Juliet Shackell edited this page Jun 24, 2024 · 51 revisions

Let’s get started right away!

First set up your computer for Salesforce CLI plugin generation, and then generate an initial sample plugin that’s ready for your custom code. Then create a few example commands, just to see how that works, and you’ll be well on your way!

Set Up Your Dev Environment

Before you generate a new Salesforce plugin, set up these prerequisites.

  1. Install or update Node.js.

    To build a Salesforce CLI plugin, you need the latest long-term support (LTS) version of Node.js. If you’re new to Node.js development, we suggest that you use nvm (Node Version Manager) to install Node.js. See this installation script to install or update nvm.

    To check your Node.js version, run:

    node --version
    

    If your node version is earlier than 8 (or if you don’t have Node.js installed), run this command to install LTS:

    nvm install –-lts
    

    Then run this command to ensure nvm always loads the installed LTS version in new terminals:

    nvm alias "default" "lts/*"
    
  2. Install the Yarn package manager.

    npm install -g yarn
    
  3. Install TypeScript (target es2017.)

    npm install -g typescript
    

    Salesforce CLI plugins can use JavaScript instead of TypeScript, but the classes in the Salesforce DX core library are written in TypeScript.

  4. Install or update Salesforce CLI.

    If you don’t have Salesforce CLI installed on your computer, see Install the Salesforce CLI in the Salesforce CLI Setup Guide. After installing, update Salesforce CLI to ensure you’re on the latest version.

    sf update
    
  5. We recommend you use Visual Studio Code with Salesforce Extensions as your IDE, because it includes tools for developing on the Salesforce platform.

Generate a Plugin and Run the Hello World Command

Use the interactive plugin generator, which is itself a plugin, to create your own Salesforce CLI plugin.

  1. Install the plugin-dev Salesforce CLI plugin, which contains commands for generating plugins, commands, flags, and more:

    sf plugins install @salesforce/plugin-dev
    
  2. Change to the top-level directory where you want to generate your plugin. For example:

    cd /Users/astro/salesforce-plugins
    
  3. Run the interactive command:

    sf dev generate plugin
    

    You're first prompted whether you're an internal Salesforce developer; we often use this tool to create our plugins too! But you should answer n so you're not subject to our internal requirements. You're next prompted for information to populate your new plugin, such as its name, description, author, and percentage of code coverage you want. The command will clone either the plugin-template-sf or plugin-template-sf-external Github repo and install the plugin's npm package dependencies using yarn install.

    When the generate command completes, the new plugin contains an example sf hello world command. See this section for a description of some of the generated files.

  4. Change to the new plugin directory, which is the same as the name you provided.

    cd my-plugin
    
  5. To run the commands in your in-development plugin from the top-level directory that your code lives in, precede the commands with bin/dev.js. For example, to run the sample hello world command:

    bin/dev.js hello world
    bin/dev.js hello world --name Astro
    

    To view the --help output for the command:

    bin/dev.js hello world --help
    

    As you create new commands, test them the same way. For example:

    bin/dev.js create webcart
    
  6. When you’re ready to test-drive your plugin, link your in-development plugin to Salesforce CLI. From the top-level plugin directory, such as my-plugin, run this command (be sure to include the period at the end!):

    sf plugins link .
    

    The command installs your plugin in Salesforce CLI by creating a symlink to your my-plugin directory. After you link your plugin, you can run your commands without using bin/dev.js. For example:

    sf hello world
    sf hello world -h
    

    You might see this warning; don't worry, it's expected and everything is working just fine:

    Warning: my-plugin is a linked ESM module and cannot be auto-transpiled. Existing compiled source will be used instead.
    

    This warning means that your plugin is written with ESM (officially called ECMAScript modules), which is what our templates use. We can't auto-compile ESM when running a command, which means you must ensure that your plugin has been compiled after every change. We recommend running yarn compile --watch in a separate terminal so that all your changes are compiled every time you save a file.

    The -h flag, which displays a shorter version of help messages, is available only when you link or install the plugin. It's not available when using bin/dev.js.

    To see which plugins you've installed or linked, including your new plugin, run:

    sf plugins --core
    

    Your linked plugin is listed like this in the output:

    my-plugin 1.0.0 (link) /Users/astro/salesforce-plugins/my-plugin
    

    To unlink the plugin, run:

    sf plugins unlink .
    

Tour the Generated Files

The sf plug-In Generator creates many files, some that support the entire plugin, some for the hello world command. This table describes a handful of the important ones which you can use as templates when you start coding your own commands.

File Description
package.json Npm file that describes package dependencies and versions.
tsconfig.json Typescript file that specifies the root files and the compiler options required to compile the project.
src/commands/hello/world.ts Main TypeScript file that contains the code for the hello world command. The command imports and extends classes from @salesforce/sf-plugins-core. When you add your own commands, you use the SfCommand abstract class.
messages/hello.world.md Markdown file that contains the messages that make up the command help and errors.
test/commands/hello/world.test.ts Unit tests.
test/commands/hello/world.nut.ts Complex integration, smoke, and end-to-end tests. Also known as NUTS (non-unit-tests.)
.github/workflows/*.yml Sample GitHub Actions workflow files for testing, releasing, and publishing your plugin. See this repo for how the Salesforce CLI developer team uses GitHub Actions.

Add More Commands

Saying hello to the world is always fun, but I’m sure you're ready to do more. Let's add two more commands to your plugin.

Call an External Service

Let's create a new command call external service that makes an HTTP request to an external service that returns an interesting fact about a number. The command has no flags other than the ones you get for "free" (--help, -h and --json). Note that when you enter the service's URL in your browser, it displays JSON with the interesting fact and some metadata about it. Okay, let's get going!

  1. Install Got, our recommended npm HTTP library, into your plugin by running this command:

    yarn add got
    

    Because the new command makes HTTP requests, you need an HTTP library. While you can use any library you want, we recommend Got because it’s simple yet full featured.

  2. In a terminal, change to the top-level directory of your plugin and run this command to generate the initial files (AKA scaffolding):

    sf dev generate command --name call:external:service
    

    The command prompts if you want to overwrite the existing package.json file, enter y so the file is updated with information about the new command.

    The --name flag specifies the full colon-separated name of the command you want to create. For example, if you want to create the command do awesome stuff, set --name to do:awesome:stuff.

    The command generates these files, similar to the files associated with the hello world command:

    • src/commands/call/external/service.ts
    • messages/call.external.service.md
    • test/command/call/external/service.nut.ts
    • test/command/call/external/service.test.ts

    The Typescript files contain import statements for the minimum required Salesforce libraries, and scaffold some basic code. The new type names come from the value you passed to the --name flag.

  3. Open up src/commands/call/external/service.ts in your IDE, such as Visual Studio Code.

  4. Add this import statement to the top of the file with the other import statements:

    import got from 'got';
  5. Let’s now handle the JSON returned by our new command when a user runs it with the --json flag, which all CLI commands have by default. We want our command to return the JSON provided by the external service’s API. The underlying CLI framework uses the return type of the command class’ run method, which in this case is CallExternalServiceResult. So replace the code for CallExternalServiceResult to reflect the service’s JSON:

    export type CallExternalServiceResult = {
      text: string;
      number: number;
      found: boolean;
      type: string;
    };
  6. Let’s next work with the command flags. Remember that our command doesn't have any flags beyond the default ones, so remove this code inside the CallExternalService class which declares a flag that we no longer need:

        public static readonly flags = {
          name: Flags.string({
            summary: messages.getMessage('flags.name.summary'),
            description: messages.getMessage('flags.name.description'),
            char: 'n',
            required: false,
          }),
        }

    Then remove the Flags import from @salesforce/sf-plugins-core at the top of the file; the import statement should look like this:

    import { SfCommand } from '@salesforce/sf-plugins-core';
  7. The final coding step is to update the run method inside the CallExternalService class. This method is where you put all of your logic for your command. In our case, we want our command to make an HTTP GET request to the service API and return the result. Replace the run method with this code:

    public async run(): Promise<CallExternalServiceResult> {
        const result = await got<CallExternalServiceResult>(
          'http://numbersapi.com/random/trivia?json'
        ).json<CallExternalServiceResult>();
    
        this.log(result.text);
    
        return result;
    }

    In the preceding code, note that this.log logs the interesting fact to the terminal only if the –json flag is not provided. In other words, the underlying CLI framework is smart enough to know whether to log non-JSON output, like strings, to the terminal.

  8. We're ready to test! Open a Terminal, either in VS Code or outside, and run the command using bin/dev.js.

    bin/dev.js call external service
    

    Hopefully you’ll see an interesting fact like this:

    54 is the number of countries in Africa.

    Let’s try the freebie flags, the ones that are magically available even though you didn't explicitly add them. First get help for the command:

    bin/dev.js call external service --help
    

    The help message isn't very useful yet because you haven't updated the boilerplate content in messages/call.external.service.md. We'll show an example of updating the help in the next example. Now let's run the command again but get JSON output instead of the default human-readable:

    bin/dev.js call external service --json
    

Good job adding a new command! Now let’s create something more Salesforce-y.

Connect to a Salesforce Org

Let's next create a more complex command that connects to a Salesforce org and displays a list of all Account names and IDs.

The command has one new flag, --target-org, with short name -o, for specifying the username or alias of an org you’ve previously logged into (AKA authorized) with the login org command. Do you recognize the flag? It's a standard flag that many of the core Salesforce CLI commands use, such as project deploy start. You can use the flag too, including its existing code to connect to an org. Let's give it a try.

  1. In a terminal, change to the top-level directory of your plugin and run this command to generate the initial command files; answer y to overwrite the package.json file:

    sf dev generate command --name connect:org
    

    Here's the list of generated files:

    • src/commands/connect/org.ts
    • messages/connect.org.md
    • test/command/connect/org.nut.ts
    • test/command/connect/org.test.ts
  2. Run this interactive command to generate a flag. In this context, "generate" means update the existing command files with the required information for the new --target-org flag:

    sf dev generate flag
    

    Be sure you specify these properties of the new flag when prompted:

    • Command to add a new flag: connect org
    • Type: requiredOrg
    • Use standard definition: Y
    • Flag name: --target-org (default)

    The command updates two files: src/commands/connect/org.ts with the new flag code and messages/connect.org.md with a new entry for the flag's summary, which is displayed when you run the command with the --help flag.

  3. Open src/commands/connect/org.ts in your IDE and review the new generated code for the --target-org flag:

        'target-org': Flags.requiredOrg(),

    Because we're using existing code for the flag, the single line of code is all you need -- magic!

  4. Because org.ts still contains code for the --name flag, which was added automatically when you originally generated the command, remove it now, just to keep things tidy. This is the code you can remove from public static readonly flags:

    name: Flags.string({
      summary: messages.getMessage('flags.name.summary'),
      description: messages.getMessage('flags.name.description'),
      char: 'n',
      required: false,
    }),

    The new flag is ready! Let's now code the command functionality.

  5. Update the command’s return type (ConnectOrgResult) with this code:

    export type ConnectOrgResult = Array<{ Name: string; Id: string }>;
  6. Update the run method with this code:

    public async run(): Promise<ConnectOrgResult> {
      // parse the provided flags
      const { flags } = await this.parse(ConnectOrg);
    
      // Get the orgId from the Org instance stored in the `target-org` flag
      const orgId = flags['target-org'].getOrgId();
      // Get the connection from the Org instance stored in the `target-org` flag
      const connection = flags['target-org'].getConnection();
    
      this.log(`Connected to ${flags['target-org'].getUsername()} (${orgId}) with API version ${connection.version}`);
    
      // Use the connection to execute a SOQL query against the org
      const result = await connection.query<{ Name: string; Id: string }>('SELECT Id, Name FROM Account');
    
      // Log the results
      if (result.records.length > 0) {
        this.log('Found the following Accounts:');
        for (const record of result.records) {
          this.log(`  • ${record.Name}: ${record.Id}`);
        }
      } else {
        this.log('No Accounts found.');
      }
    
      return result.records;
    }

    Your new command should now be ready to test!

  7. If you haven't already, log into your test org with the login org web command. Run org list to see the orgs you've already logged into and their associated usernames and aliases. We'll use the username or alias to test your new command.

    Logging into an org before you work with it is standard Salesforce CLI practice. Think about project deploy start: it too takes a username (also via the --target-org flag) of the org you want to deploy to, and it's assumed that you've previously logged into it.

  8. Test your new command with bin/dev.js; replace <org-username-or-alias> with the username or alias of the org you just logged into:

    bin/dev.js connect org --target-org <org-username-or-alias>
    

    Hopefully you'll see output similar to this (the record IDs have been truncated for security):

    Connected to <your-org-username> (<your-org-id>) with API version 61.0
    Found the following Accounts:`
      • Acme: 001B...`
      • salesforce.com: 001B...`
      • Global Media: 001B...
      • TestAccount: 001B...
      • xyz: 001B...
      • Test1: 001B...
    

    Make sure that the short flag name works too!

    bin/dev.js connect org -o <your-org-username>
    

Pretty cool, huh.

Next Steps

Congratulations! You've successfully generated a Salesforce CLI plugin and added a few commands. Now it's time to customize it for your specific purpose. Here are the next steps on this exciting journey:

Clone this wiki locally