My first Salesforce CLI Plugin Part 2—Using Node.js to read files from an sfdx project directory

This is the 2nd 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 1—The idea and progress so far
Join me as I document my journey to creating my first Salesforce CLI plugin. It’s going to change the world…

In this article, I want to talk a little bit about using Node.js to read files from an sfdx project directory.

How I started developing this project

Initially, I created a script inside a .js file in my sfdx project.

I could call this file by running node apexFolders.js on the terminal, and all the apex classes would end up neatly organized into folders.

Note that I needed this .js file to exist within an sfdx project, otherwise, I had no way to test that it does what it does (though at a later stage, I started using Jest to mock the entire sfdx project directory, more on that in a future episode).

Here's what the code looked like at that point in time.

Let's dissect the code a bit.

Reading files with Node.js

In order to organize apex class files based on their prefix, I need to be able to read the name of the file.

To read file names and their contents, we can use the built-in fs module in Node.js, so I started by importing that module. I also defined what the default path for apex classes is in a typical sfdx project directory.

const fs = require('fs');
const OTHER_FILES = 'Other';
const CLASSES_PATH = 'force-app/main/default/classes';

What I need now is a way to have the names of all the files in the CLASSES_PATH directory, and for that, we can use this function

const files = await fs.promises.readdir(CLASSES_PATH);

The fs.promises object gives access to reading and writing functions in the form of promises. This is something I got rid of later, but I'll explain that in a future article. The more relevant thing to note is the readdir function, which reads files (and sub-directories) from a specific directory.

After this code runs, the files variable has the names (and just that) of all the files inside the default classes directory.

Determining if a class has a prefix

Now, this version of the code is very naive. Remember, I'm showing you an earlier version, which I wrote as a POC to validate my ideas. Many edge cases are not covered in this initial version.

The next step is to determine if an apex class has a prefix like fflib_UnitOfWork .

The brute force approach was to determine if the class name contained _ in it, and if so, assume that the first part is the prefix (yes, a very naive assumption, as I said above).

If it has a prefix, I want to put the file name in a list along with all other files that share the same prefix.

for( const file of files ) {

        //happysoup_className
        let parts = file.split('_');

        if(parts.length > 1){

            let prefix = parts[0];

            if(filesByPrefix.has(prefix)){
                filesByPrefix.get(prefix).push(file);
            }
            else{
                filesByPrefix.set(prefix,[file]);
            }
        }
        else{
            filesByPrefix.get(OTHER_FILES).push(file);
        }
    }

filesByPrefix is a map that's supposed to contain all the apex classes for a given prefix, like this:

fflib => ['fflib_UnitOfWork','fflib_AccountService']
happysoup => ['happysoup_DependenciesController']

If a file name does not have a prefix, I store it in a map where the "prefix" is the word "Other", like this:

Other => ['AccountController','ConvertLeadBatch'...]

Creating the prefix folders

At this stage, the keys in the filesByPrefix map are the names of all the subfolders I have to create.

So, the next step is to iterate over those keys. To iterate over the keys of a map, you need to extract the keys and convert the set into an array using the Array.from method, like this:

let keys = Array.from(filesByPrefix.keys());

Now, I need to iterate over those keys and start coming up with the new folder names

await Promise.all(keys.map( async (prefix) => {

        let domainFolder = `${CLASSES_PATH}/${prefix}`;
        let sourceFolder = `${domainFolder}/src`;
        let testFolder = `${domainFolder}/tests`;
    ...
}

The promise.all and keys.map is something I got rid of later. Again, I will explain why in a future chapter. For now, just know that I'm looping over those keys, each of which represents a prefix.

Having the prefix variable, I can create the full paths of the new folders I need to create.

First, I need to create a "domain" folder under the default apex class directory. For example, if the prefix is fflib, I want a folder the following folder to be created

force-app/main/default/classes/fflib

Then, inside each domain folder, I want a src and tests folder, where I'll add non-tests and test classes for that domain respectively. So that's what the sourceFolder and testFolder are in the code below

let sourceFolder = `${domainFolder}/src`;
let testFolder = `${domainFolder}/tests`;

Note that their parent folder is the newly created domainFolder , which will result in 2 folders

force-app/main/default/classes/fflib/src

and

force-app/main/default/classes/fflib/tests

Once I have the desired path of the new folders, I can create them using the fs.mkdirSync function

 fs.mkdirSync(domainFolder);
 fs.mkdirSync(sourceFolder);
 fs.mkdirSync(testFolder);

Moving the files to their new location

To move files to another directory, I need to use the fs.promises.rename function (yes, that's a weird name...)

This function needs 2 parameters: the original location of the file, and the new one.

So I start by creating a variable to hold the original location of the apex file

let originalLocation = `${CLASSES_PATH}/${file}`;

Then, I need to figure out what the new location will be, either the sourceFolder or the testFolder, depending on whether the class is a test class.

For that, I have this naive bit of logic (much more complex in the current version of this code)

 if(file.toLowerCase().includes('test')){                
     newLocation = `${testFolder}/${file}`;
 }
else{
    newLocation = `${sourceFolder}/${file}`;
}

Basically, if the file name includes the word test, the new location is the test folder. Otherwise, it's the source folder.

💡
Again! I know this doesn't account for a class name containing the word testimonial, etc. This was a POC at the time. 

Once I have the new location, I can use the following function, which moves the file to its new folder

await fs.promises.rename(originalLocation,newLocation);

And here's the end result

As I said earlier, the Other folder contains all the apex classes that didn't have a prefix.

So that's it!

What's next

Testing this with a real sfdx project is time-consuming because if I made changes to the script, I had to get rid of the newly created folder structure and start from scratch.

In the next chapter, I'll talk about how I'm using Jest to mock the entire sfdx project directory.

If you want me to let you know when the next part comes up, consider subscribing!

Subscribe for exclusive Salesforce Engineering tips, expert DevOps content, and previews from my book 'Clean Apex Code' – by the creator of HappySoup.io!
fullstackdev@pro.com
Subscribe