DEV Community

Extending the Salesforce CLI with a Custom Plugin

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, 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—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. You should install the latest version available, such as 12.20.0 or 14.13.1
  • The Salesforce CLI tool, installed globally as the sfdx-cli NPM package
  • TypeScript (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.

Next, if you don’t have a Salesforce org, sign up for a free Developer Edition org. Enable Dev Hub for your Salesforce 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
Enter fullscreen mode Exit fullscreen mode

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, 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"]
-    },
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 $USERNAMEis 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 bin/run to execute our hello:org command. bin/run executes sfdx, while loading a local 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 hello. Navigate to the src/commands directory in your recently scaffolded project, and you’ll see a directory called hello, with a single file, org.ts.

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

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';
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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
   }),
 };
Enter fullscreen mode Exit fullscreen mode

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 countis 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. :)";
 }
}
Enter fullscreen mode Exit fullscreen mode

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"
 ]
}
Enter fullscreen mode Exit fullscreen mode

In order to better understand what this JSON file does, execute bin/run fixtures:generate --help 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: mapping these strings to commands and flags for a better UX. 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);
   });
});
Enter fullscreen mode Exit fullscreen mode

Here, we’re executing the command through the command('fixtures:generate') method call. Then, we’re using Node’s access method to verify that our JSON files were created. Go ahead and call npm test, 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
Enter fullscreen mode Exit fullscreen mode

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_NAMEto 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
Enter fullscreen mode Exit fullscreen mode

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:

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; I even found an entire category of amazing things you can do with sfdx. In addition, their documentation includes some best practices and performance suggestions. Happy coding!

Discussion (0)