My first Salesforce CLI Plugin Part 4—The new sf plugin architecture and refactoring the project

This is the 4th of part of this series, where I document my journey to creating my first Salesforce CLI plugin.

Remember, the CLI plugin is meant to reorganize apex classes based on their prefix, so classes like fflib_UnitOfWork and fflib_Service would be grouped inside a fflib folder in an sfdx project directory.

Read part 1 to get more info on the use case and rationale.

You can read the previous parts here:

My first Salesforce CLI Plugin Part 3—Mocking an sfdx directory with Jest
In this entry, I explain how I used Jest to mock the file structure of an sfdx project

Where are we

In the previous article, I talked about testing the plugin logic using Jest. At this point, I'm confident that all the logic works as expected; I also tested it manually with a real sfdx project.

Now, I need to create the actual plugin; remember that up to this point, I've been working on a simple NPM package as I wanted to avoid getting dragged down by details of how the plugin architecture works, etc.

So let's talk about how that's going.

New SF plugin architecture

In case you are not aware, Salesforce is moving away from the sfdx CLI, and is currently implementing the new sf CLI. The same is true for plugins; they should be created under the sf CLI and their architecture has changed.

There's also a new developer guide to building sf plugins, currently hosted on GitHub.

Creating the plugin

To create the plugin, I used the sf dev generate plugin command, which asks you a few questions about your plugin and generates the entire directory with all the files ready to go.

One thing to note is that this directory has many more files compared to the old plugin architecture.

This is because this new architecture comes with built-in support for CI/CD, so there's a ton of configuration files and yarn commands that support that process.

Once the basic structure of the plugin is created, you can create new commands using the sf dev generate command --name [name:of:command] command.

In my case, I created a command called dxdir:arrange, which should be called like this sf dxdir:arrange. Again, all the files needed to run this command were automatically generated, which is awesome

Finally, remember that the code is meant to act on the context of a folder where the apex classes reside, and by default it's assumed to be force-app/main/default/classes.

For this, I created a flag so that I could call the command like this sf dxdir:arrange --apex-dir [your path]

Again, the new sf CLI provides a handy interactive command to create flags; the sf dev generate flag command.

I created a flag called apex-dir, of type directory and configured it so that it automatically verifies if the directory actually exists or not (I didn't configure any of this manually; the CLI asked me these questions when I ran the generate flag command.

This resulted in the following flag

The exists property verifies that the directory actually exists, and there's also a default value, which I set to the default apex class directory.

Transferring the code

The final step was to move the code from the original NPM package into this new folder. This wasn't too easy, and I had to change a few things to get it to work, but it did in the end :)

Passing the flag

Once I copied all the existing code into the command file, I modified the run method to call the original code, and pass the flag that points to the folder where the classes reside

Here, reoderFiles is the original function I wrote in previous chapters; the difference is now we use the flag passed to the command. You can see we extract the value of the flag using the flags[apex-dir syntax.

As mentioned earlier, if the directory doesn't exist, the flag automatically throws an error, which is great because it means I don't need to add more code to validate that myself.

Getting the tests to work

When you generate a new command, the CLI also creates a set of tests files for that command. Here's what a default test looks like

Basically, this test says that when the command dxdir:showme is run, we can expect the standard output (the terminal) to print something that contains the words hello world.

This is very different from how I wrote my tests, as covered in part 3. Also, these tests are written with the testing library Chai, instead of Jest (what I used).

So here's what I did to configure the plugin to run Jest tests my way:

1- Configure Jest to work with Typescript

2- Then, I created a new NPM command called jest-test that would call Jest.

3- Then, I had to modify the pre-push husky file (which runs before I push the code to a remote repository) to run the new NPM command.

This basically means that when I run git push, the yarn build and yarn jest-test commands will run, which build the code (this is a typescript project) and run all tests; if everything passes, the code is pushed to the remote repo.

4- Finally, the sf CLI comes with built-in test and expect functions from Oclif (the project that supports the CLI) that you are meant to use to test if the output of your command is what you expect.

These functions had the same names as those from Jest, which resulted in a bunch of weird errors, such as test not found or expect is not a function.

I want to be able to use these built-in functions in the future (not sure yet how) while still using the standard Jest functions with the same name.

What I did was give them an alias of oTest and oExpect. This way, I can use Jest's test and expect functions, and in the future, when I want to use the Oclif ones to verify the terminal output, I can use oTest instead

With all that done, I can now run yarn jest-test and all my tests pass again~

What's next

I think this series is long enough, so the next chapter will be the last chapter; it'll probably be a summary of some new things I added, such as new flags or new commands, and an overview of how to build and publish the plugin.

Thanks for following my journey!  

Subscribe for exclusive Salesforce Engineering tips, expert DevOps content, and previews from my book 'Clean Apex Code' – by the creator of!