How to Write JavaScript Code to Create and Update a Changelog | by Matthew Croak | Apr, 2022

No more generators, logs, or dependencies-now for JavaScript!

Photo by Greg Rakozy on Unsplash

If you read my previous post about how to write a python script to create and update a changelog, or the post before that about writing a changelog script in bash, you might find this post interesting as well. The goal is to add support to multiple languages ​​so developers don’t have to choose just one rather than they can choose the one that is most in line with the stack they’re currently using.

I’ve added JavaScript support to extend my original changelog script to engineers with a preference for JavaScript. You can now use the same script logic with JavaScript. If you’re a JS developer and want to add more customizations to your changelog creation/generation, you can do so more easily now with this added support.

Since it’s the same logic as my last two posts related to Bash and Python, I won’t go too in depth in terms of functionality. Like the one on Python, this post will highlight syntactic differences.

First, create a changelog.js file to run the script’s logic. You can make this file executable by running chmod +x changelog.js. With this you will be able to run ./changelog.js from your command line or include it in another script if that’s preferred by appending ./changelog.js to it.

Next, we need our project’s version to be synced with our changelog. This is probably the most complicated part of the JavaScript code. You can do one of two things:

  1. Find a package (something like this maybe) that can be used to execute git describe --long in a JavaScript file.
  2. Be a purist and do it all with your own code! If this is the route you’re going, we have a few steps.

First, we have to require what is called child_process and then run exec on that child process. This method allows us to run commands in the console and buffer the output. Here is our first line of code.

const exec = require('child_process').exec;

Next, we need to write a function that runs a child execution that accepts "git describe --long" as an argument, along with a callback function. Below is our function.

const result = (command, cb) => {
var child = exec(command, (err, stdout, stderr) => {
if(err != null){
return cb(new Error(err), null);
}else if(typeof(stderr) != "string"){
return cb(new Error(stderr), null);
}else{
return cb(null, stdout);
}
});
return child
}

This is basically a wrapper that will execute a passed command along with the callback function. This wrapper also offers preliminary error-handling. All we have left to do is write a function that actually executes this sequence and handles the result. We’ll call this function getTaggedVersion.

Here is the code.

const getTaggedVersion = () => {
result("git describe --long", (err, response) => {
if(!err){
console.log(response);
}else {
console.log(err);
}
});
}

It’s a bit convoluted, but it can save you from having to install and configure a dependency! Now, onto the rest of our script.

There are places in changelog.js where we use string interpolation. This is done a little differently in JavaScript than it is in Bash or Python. See below for Bash string interpolation.

name="Matthew Croak"
introduction="My name is $name"
echo $introduction
> "My name is Matthew"

Finally, here is string interpolation for JavaScript.

name="Matthew Croak"
introduction=`My name is ${name}`
console.log(introduction)
> "My name is Matthew"

String interpolation is important for the script because we will be writing and rewriting the changelog file from provided strings. These strings will contain updated information, such as today’s date and the version of the package.

Next, let’s write our init function. This function will be used to determine whether or not we want to create a new changelog or simply update an existing one. In JavaScript, the fs module makes file handling relatively simple. We can import existsSync from the fs module and check if a provided path exists. See below.

const {existsSync} = require('fs');
const path = './CHANGELOG.md'
const init = () =>{
try {
if (existsSync(path)) {
newChangelogItem()
} else {
newChangelog()
}
} catch (err){
console.log(err)
}
}

Next, let’s go over how we read and write our changelog file. In Bash, this is how you read the file:

while read line; do           
# code performed for each line
done < CHANGELOG.md

In JavaScript, we make use readFileSync to read a provided file, and writeFileSync to write to a provided file. Both functions are provided by the fs module. readFileSync will essentially take a provided file and convert it to a string. We can then take this string and split it at every new line (/n).

This way, we can create an array of lines to iterate over. See below.

const fileData = readFileSync("CHANGELOG.md", { encoding: "utf8" });
const fileDataArray = fileData.split("n");

writeFileSync looks similar but it requires an extra argument (between the filename and the encoding data). This third argument is the provided string that will be written into the file. See below.

writeFileSync("CHANGELOG.md", newFileData, { encoding: "utf8" });

Before we continue on with readAsync and writeAsynclet’s go over one more function provided by the fs module: appendFile.

We can use appendFile to, as the name implies, append file data to a file. By provided a filename, file data (as a string) and a callback function, we can create our changelog file with ease. See below.

const newChangelog = () => {
appendFile("CHANGELOG.md", changelogStart, (err) => {
if (err) throw err;
console.log('Changelog is created successfully.');
})
}

This function will create a new file (CHANGELOG.md), populating it with changelogStart (the string that is available in the changelog.js file on Github). Now that we made our function which will create a changelog for us, should one not exist, we can move onto our function to add a new changelog item to an existing changelog!

Now, before we get into the line insertion code, let’s see what code we need to determine whether or not we should add an item. Remember, our changelog items correspond to a project version and date. If we already have a changelog item for the current version and date, we don’t need to create a new one.

Below is the code we’ll use to check if we need a new item.

const newChangelogItem = () => {
if (checkIfItemExists(item)){
console.log(`Changelog item already exists for n ${item}`)
} else {
writeChangelog()
}
}

We’ll get to writeChangelog in a moment, but first, here’s checkIfItemExists.

const checkIfItemExists = (str) => {
const contents = readFileSync('CHANGELOG.md', 'utf-8');
return contents.indexOf(str) > -1;
}

This function makes use of readFileAsync. This will generate a stringified version of our changelog. Once it’s a string, we can use check the index of our changelog item. If it’s greater than -1, then it exists and we don’t need to add a new one. Otherwise, add a new one. Simple, no?

Now that we are handling whether or not we can/should add a new changelog item, let’s check out how we can actually add one. For this, we will make use of readFileAsync and writeFileAsync. We will convert our file into an array of lines (as mentioned earlier), then iterate over that array if lines. While iterating over our lines, we lookout for the line where we will be inserting our new item. This line is indicated by "## [Unreleased]".

We can check this by making use of indexOf again. If fileDataArray[i].indexOf(“## [Unreleased]”) > -1 then we know that that is the line where we insert our item. How can we insert our item in JavaScript? By making use of splice.

splice allows us to add new elements in place of an existing one in our array. We can simply splice our new item into our array right after our “## [Unreleased]” line. See below.

iterateArr = [...fileDataArray)
for (var i = 0; i < iterateArr.length; i++){
if (fileDataArray[i].indexOf("## [Unreleased]") > -1){
fileDataArray.splice(i + 1, 0, `n${item}n### Addedn- ADD CHANGE HERE!`)
const newFileData = fileDataArray.join("n");
writeFileSync("CHANGELOG.md", newFileData, { encoding: "utf8" })
break
}
}

iterateArr is created from fileDataArray by using the spread operator. We do this because we don’t want to iterate over the array we are manipulating. Once we have spliced our new item into the array of lines, we can join them into one string. We can then provide this string to writeFileAsync and write it to our CHANGELOG.md file.

Which one should you use?

Given that this changelog generation now has support for Bash, Python and JavaScript, which one should you use? I personally found Python to be preferred. I feel the learning curve was Bash was a bit wider than Python (for me at least) and while Bash has fewer lines of code, I struggled more with debugging.

JavaScript is my preferred language (I am biased as I have worked for years with JavaScript professionally), but for this use case, I found I had to write a lot more configuration code. While the fs module provides plenty of built-in logic for file management and manipulation, I had to write separate functions to support the execution of git commands and I found there more steps in order to start climbing through the file and adding text.

But if you’re a JavaScripter (?) through and through, you now have a pure script to incorporate into your app to generate a changelog and add changelog items. It also offers you flexibility in terms of format and changelog content without the need for generators, logs, or other dependencies.

The full code is on GitHub.

Leave a Comment