Extending the Salesforce CLI with a Custom Plugin
Tags: webdev, devops
Published: 2021-10-20 14:31:35

As more services move to the cloud and DevOps methodologies continue to evolve, more and more developers are getting comfortable with working in the terminal. Whereas traditional CLI commands like `grep` and `cat` are well-known tools for achieving small goals, more complicated tasks require more robust tooling.

Nowadays, CLI programs come with richer interactive experiences. One such program is the [Salesforce CLI](https://developer.salesforce.com/tools/sfdxcli), the command-line interface for Salesforce DX. It’s a CLI that helps simplify common operations when building an application for Salesforce developers. The Salesforce CLI itself is built on top of [oclif](https://oclif.io/)—also from Salesforce—which is an open-source framework for building command-line interfaces. Oclif is written in TypeScript, and it has a very robust community supporting it. Best of all, it has an architecture that prioritizes plugin support, which enables users to extend the CLI for their own needs.

In this post, we’ll create a custom plugin for sfdx. Our plugin will generate fixture data for a Lightning app that runs on a Salesforce org. This data will be used to populate our project with random names and addresses in order for us to test the usability of our project.


## Prerequisites

Before getting started, install the following:


* A relatively recent version of [Node.js](https://nodejs.org/en/). You should install the latest version available, such as 12.20.0 or 14.13.1
* The [Salesforce CLI tool](https://developer.salesforce.com/docs/atlas.en-us.234.0.sfdx_setup.meta/sfdx_setup/sfdx_setup_install_cli.htm#sfdx_setup_install_cli), installed globally as the `sfdx-cli` NPM package
* [TypeScript](https://www.typescriptlang.org/) (As long as you know JavaScript, deep familiarity with TypeScript isn’t required.)

To make absolutely sure that you have all the necessary software, follow [this guide on preparing for Salesforce CLI plugin development](https://developer.salesforce.com/docs/atlas.en-us.sfdx_cli_plugins.meta/sfdx_cli_plugins/cli_plugins_generate_prepare.htm).

Next, if you don’t have a Salesforce org, [sign up for a free Developer Edition org](https://developer.salesforce.com/signup).  [Enable Dev Hub for your Salesforce ](https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_setup_enable_devhub.htm)org. Once your DevHub is enabled, you’ll need to associate it with your Salesforce CLI install. To do that, run the following command:


```
sfdx auth:web:login -d -a DevHub
```


This will open a new browser window that will ask for your Salesforce credentials. Once you’ve gone through that flow, sfdx will inform you when the authentication is complete. 

Finally, you can [create a scratch org](https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_scratch_orgs_create.htm), which is like a temporary Salesforce org. You can use the scratch org to test what developing on the Salesforce platform looks like.


## Getting started

Open up a terminal window, and navigate to a folder for your new project. Then, clone the following GitHub repository:

```
git clone https://github.com/trailheadapps/dreamhouse-lwc.git
```

The Dreamhouse app is a sample Lightning application that lists realtors and their associated properties. The app comes with some fixture data, but since _our_ goal is to build an sfdx plugin that creates a set of randomized fixture data for this application, we can get rid of these files. To do so, delete the following files from your newly cloned repository:

```
data/brokers-data.json
data/properties-data.json
```

You should also remove their references from the data/sample-data-plan.json file; a diff of that would look like this:

```
--- a/data/sample-data-plan.json
+++ b/data/sample-data-plan.json
@@ -1,17 +1,9 @@
 [
-    {
-        "sobject": "Broker__c",
-        "saveRefs": true,
-        "files": ["brokers-data.json"]
-    },
-    {
-        "sobject": "Property__c",
-        "resolveRefs": true,
-        "files": ["properties-data.json"]
-    },
```

Next, let’s see what the app actually looks like. Run the install script which comes with the repository—either `bin/install-scratch.sh` or `bin/install-scratch.bat`, depending on your operating system—to push the complete Lightning app to your Salesforce org.

This opens a browser window and takes you to your Salesforce instance. Click on the **App Launcher** icon in the upper left corner (it’s the row of dots) and select the Dreamhouse app. You should see a Quarterly Performance chart, without any data. You can also click on the **Properties** menu, and note that there are zero items available.

This is all to be expected. Our Salesforce CLI plugin will create the fictional properties which our app will use.


### Creating the plugin

In a new folder outside of the dreamhouse-lwc project, run this command:

```
sfdx plugins:generate fixture-data-demo
```

This will scaffold a new plugin in a directory called `fixture-data-demo`. You’ll be asked a series of questions, but you can just keep hitting Enter to accept all of the defaults.

After npm finishes installing all of the dependencies, we can test whether or not the plugin was set up correctly. Run `bin/run hello:org -u $USERNAME`, where `$USERNAME `is your Salesforce user name. You should see a greeting in response.

One of the nice things about extending the Salesforce CLI is that many of the difficult choices around working with command flags or arguments are vastly simplified for you. There’s even a test runner that requires very little configuration on your part. Let’s start building out our plugin to see these features in action.

You might have noticed two interesting things about the command we used to run to get our earlier greeting:


1. We used <code>bin/<strong>run</strong> </code>to execute our <code>hello:org</code> command. <code>bin/<strong>run </strong></code>executes sfdx, while loading a <em>local </em>copy of your plugin. That is, the plugin you are developing can only be used in this directory; it’s not available outside of this scope just yet.
2. All sfdx commands are preceded by a namespace to avoid conflicts with other plugins. In this case, that namespace is <code>hello</code>. Navigate to the <code>src/commands</code> directory in your recently scaffolded project, and you’ll see a directory called <code>hello</code>, with a single file, <code>org.ts</code>.

Let’s suppose we want our plugin to run as <code>sfdx fixtures:<strong>generate</strong></code>. In that case, rename the <code>hello</code> folder to <code>fixtures</code>, and the <code>org.ts</code> file to <code>generate.ts</code>.

You can delete all of the code that is in the newly renamed `generate.ts`. In the next part of this guide, we’ll provide code blocks that you should paste into the file instead. As we progress, we’ll describe what our code does so that you can follow along.

First, let’s set up our `import` statements:

```
import * as os from 'os';
import { promises as fsPromises } from 'fs';
import * as path from 'path';

import { flags, SfdxCommand } from '@salesforce/command';
import { Messages } from '@salesforce/core';
import { AnyJson } from '@salesforce/ts-types';

import * as mkdirp from 'mkdirp';
import * as Chance from 'chance';
```

The first set represents core Node modules that we’ll need to use in our CLI plugin. The second set represents core modules required by sfdx. Finally, the third set represents third-party npm packages that our plugin needs to function. Let’s not forget to add those module dependencies to our project.

```
npm install mkdirp chance --save
```

Moving on, paste these lines after the `import` statements:

```
// Initialize Messages with the current plugin directory
Messages.importMessagesDirectory(__dirname);

// Load the specific messages for this file. Messages from @salesforce/command, @salesforce/core,
// or any library that is using the messages framework can also be loaded this way.
const messages = Messages.loadMessages('fixture-data-demo', 'generate');
```

We’ll get to the use of this `messages` variable shortly. For now, keep it in mind, and continue pasting these code blocks:

```
export default class Generate extends SfdxCommand {
 public static description = messages.getMessage('commandDescription');

 public static examples = messages.getMessage('examples').split(os.EOL);

 protected static flagsConfig = {
   // flag with a value (-n, --name=VALUE)
   count: flags.integer({
     char: 'c',
     description: messages.getMessage('numberFlagDescription'),
     default: 10
   }),
 };
```

Okay, now we’re getting to some good stuff. Our generate command is mapped to a class called `Generate`, which extends the `SfdxCommand` class. Because of this inheritance, a lot of functionality is provided for us by sfdx. We set up some variables for a command description and for examples—since this deals with `messages`, we’ll elaborate on this part shortly. More importantly, we are setting up a flag called `count`, which has a shortcode of `n`. That means that we can call our command with a `--count` or `-c` flag in order to provide some additional configuration. In this case, if no `count `is provided, the default value will be `10`. 

Next, we’ll get to the fixture data generation:

```
   const chance = new Chance();

   const brokerData = { records: [] };
   const propertyData = { records: [] };

   for (let i = 0; i < number; i++) {
     const brokerName = chance.name();

     brokerData.records.push({
       "attributes": {
         "type": "Broker__c",
         "referenceId": `${brokerName.replace(/\s/g, "")}Ref`
       },
       "name": brokerName,
       "Title__c": "Senior Broker",
       "Phone__c": chance.phone(),
       "Mobile_Phone__c": chance.phone(),
       "Email__c": chance.email(),
       "Picture__c": `https:${chance.avatar()}`
     });

     const address = chance.address();
     propertyData.records.push({
       "attributes": {
         "type": "Property__c",
         "referenceId": `${address.replace(/\s/g, "")}Ref`
     },
     "Name": chance.sentence({ words: 5 }),
     "Address__c": address,
     "City__c": chance.city(),
     "State__c": chance.state(),
     "Zip__c": chance.zip(),
     "Price__c": chance.floating({ fixed: 2, min: 100000, max: 1000000 }),
     "Beds__c": chance.natural({ min: 1, max: 5 }),
     "Baths__c": chance.natural({ min: 1, max: 5 }),
     "Location__Longitude__s": chance.longitude(),
     "Location__Latitude__s": chance.latitude(),
     "Picture__c": `https://s3-us-west-1.amazonaws.com/sfdc-demo/realty/house${chance.natural({ min: 1, max: 10 })}.jpg`,
     "Thumbnail__c": `https://s3-us-west-1.amazonaws.com/sfdc-demo/realty/house${chance.natural({ min: 1, max: 10 })}sq.jpg`,
     "Tags__c": "victorian",
     "Description__c": chance.sentence({ words: 10 })
     });
   }

   await mkdirp('data');
   await fsPromises.writeFile(path.join('data', 'brokers-data.json'), JSON.stringify(brokerData, null, 2));
   await fsPromises.writeFile(path.join('data', 'properties-data.json'), JSON.stringify(propertyData, null, 2));

   return "All done! Check the data directory. :)";
 }
}
```

This looks like a lot of code, but don’t worry! Our Dreamhouse app has several custom objects. Among those objects are Brokers and Properties. We need to provide values for their individual attributes. We create some variables to store this information as JSON (`brokerData` and `propertyData`). Then, we loop through this as many times as matches our `number` value, and we use our previously imported `Chance` package to generate random values for names, phone numbers, addresses, and so on. When that’s finished, we create a directory called `data`, and write these fixtures out as JSON.

Okay, as promised, let’s go back to that `messages` variable. You should have a folder in your plugin called `messages`, with a file called `org.json`. Rename this file to `generate.json`, and open it up. Here is where all of the help strings are defined for your CLI command. Replace the contents with the following JSON:

```
{
 "commandDescription": "generate a list of addresses and names",
 "countFlagDescription": "number of items to generate",
 "examples": [
   "sfdx fixtures:generate --count 5"
 ]
}
```

In order to better understand what this JSON file does, execute <code>bin/run fixtures:<strong>generate</strong> --help</code> at the command line in our terminal. You will see these strings displayed as help documentation. That’s the purpose of all the message code noted above: <em>mapping these strings to commands and flags for a better UX</em>. It’s important to provide your users with some guidance on how to use your command.


### Testing and Releasing

Of course, no piece of software is complete without tests. The Salesforce CLI comes with a testing library that makes it easier to build out this essential part of development.

Much like the source code, our directory and file structure should match our command name. Open up the `test/commands` directory and rename `hello` to `fixtures`, and `org.test.ts` to `generate.test.ts`.

Our entire test file will be rather small, so we won’t go into it line-by-line the way we did with the source code.

```
import { expect, test } from '@salesforce/command/lib/test';

import * as path from 'path';
import { promises as fsPromises } from 'fs';

const dataPath = path.join(__dirname, '..', '..', '..', 'data');

describe('fixtures:org', () => {
 test
   .stdout()
   .command('fixtures:generate')
   .it('runs fixtures:generate', async (ctx) => {
     const brokersFilename = path.join(dataPath, 'brokers-data.json');
     const propertiesFilename = path.join(dataPath, 'properties-data.json');

     // Fulfills with undefined upon success.
     expect(await fsPromises.access(brokersFilename)).to.be.equal(undefined);
     expect(await fsPromises.access(propertiesFilename)).be.equal(undefined);
   });
});
```

Here, we’re executing the command through the `command('fixtures:generate')` method call. Then, we’re using Node’s <code>[access](https://nodejs.org/api/fs.html#fs_fspromises_access_path_mode)</code> method to verify that our JSON files were created. Go ahead and call <code>npm test,</code> and you’ll see your newly created test pass! 

You might see some linting errors, but you can ignore them. Alternatively, you can edit `package.json` to remove the `posttest` script—that’s what runs the linter after your tests complete.

With our plugin code complete and tested, it’s time to see it in action. Now, you have a globally installed version of sfdx, but a locally developed plugin. We need a way to load the plugin into our global install. Fortunately, sfdx comes with a command to do just that:

```
sfdx plugins:link
```

When you run this command in your plugin directory, sfdx will load this local plugin so that you can test it out in your own Lightning app before distributing it. If you discover that you need to make changes to the plugin after it’s been linked, just run ​​`sfdx plugins:uninstall $PLUGIN_NAME `to unlink it.

Now, in the terminal, navigate back to your previously cloned directory for the Dreamhouse app. Once there, type `sfdx fixtures:generate`. You should now have a `data` directory, complete with several brand new JSON files filled with randomly generated data. Let’s go ahead and import these new changes into our org and then see the results:


```
sfdx force:data:tree:import -f data/brokers-data.json, data/properties-data.json
sfdx force:org:open
```


Just like before, navigate to the Dreamhouse app through the **App Launcher**. However, this time you’ll notice actual records, which represent the fixtures you just generated! The Brokers tab, for example, is full of all the same records as the ones generated by the plugin:

![](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ac1kjtf4ynf6k0k7hc0o.png)

### Learning more

Developing Salesforce CLI plugins benefits both developers _and _users. For developers, much of the boilerplate of providing documentation, argument handling, and testing is taken care of. Users can expect every custom plugin to behave in predictable ways, and these plugins unlock powerful new capabilities they can use to make their Lightning app development just a little bit easier.

We’ve only scratched the surface of what it means to work with Salesforce CLI. There’s a more in-depth tutorial on the ins and outs of Salesforce development over on [Trailhead](https://trailhead.salesforce.com/content/learn/projects/develop-app-with-salesforce-cli-and-source-control); I even found [an entire category of amazing things you can do with sfdx](https://trailhead.salesforce.com/en/search?keywords=sfdx). In addition, their [documentation](https://developer.salesforce.com/docs/atlas.en-us.sfdx_cli_plugins.meta/sfdx_cli_plugins/cli_plugins.htm) includes some best practices and performance suggestions. Happy coding!