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.
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:
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.
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!