Setup automatic versioning in a Javascript/Nativescript project

Cover image

When developing an application, maintaining the version of your project can be time-consuming. Let’s explore the steps to improve this process.

Use a commit message convention

The very first step to versioning is to have a proper commit message convention. An easy way to get started is to leverage tools like Commitizen and Commitlint in your project and enforce them using Husky.

Install Commitizen

This is optional and will not enforce the commit style as Commitlint would.

When you commit with Commitizen, you’ll be prompted to fill out any required commit fields at commit time.

You can install Commitizen globally in one step:

$ npm install commitizen -g

To initialize Commitizen for sensible defaults (using the cz-conventional-changelog convention), you can run this command:

$ commitizen init cz-conventional-changelog --save-dev --save-exact

You can use Commitizen to help you build your commit message by typing this command and following the steps:

$ git cz

Install Commitlint

Commitlint will help your team adhere to a commit convention. To install Commitlint in your project, run these two commands:

$ npm install --save-dev @commitlint/cli @commitlint/config-conventional

To configure Commitlint to use our convention, create a commitlint.config.js file with the following content:

module.exports = {extends: ['@commitlint/config-conventional']}

For a one-liner, you can run this command in your terminal:

$ echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.js

Install Husky

Installing and configuring Husky in your project will enforce the commit style for each commit.

To setup Husky in your project, run the following command:

$ npm install husky --save-dev

Then create a .huskyrc file in the root of your project and add the following content:

{
  "hooks": {
    "commit-msg": "commitlint -e $GIT_PARAMS"
  }
}

This will run Commitlint before each commit and validate the commit message against your convention. If the commit message is invalid, the commit will be aborted.

Generate a changelog

Now that we are following a commit message convention, we can easily generate a changelog for our project each time we issue a release.

For that, I would recommend using Standard Version, which will help you automate versioning and CHANGELOG generation.

To install Standard Version in your project, run:

$ npm i --save-dev standard-version

Then to generate your initial release version automatically, run:

$ standard-version --first-release

Standard Version will look at your commit history, generate the matching CHANGELOG file, commit the changes, and create a git tag.

For subsequent releases, run:

$ standard-version

This will not only generate/update the CHANGELOG file but also update your package.json file with the version number before committing the changes, and creating a git tag.


You can also set up a npm script to generate your release version, by adding the following script to your package.json:

"scripts": {
  "release": "standard-version"
}

NativeScript-only: Automate the update of platform-specific manifests

Now that we have an easy way to generate our changelog, our package.json reflecting the right version, we need to update the platform-specific manifests to reflect that version as well. For Android, the version is specified in the AndroidManifest.xml file. In a NativeScript project, you will typically find that file under the app/App_Resources/Android/src/main directory. Look for the versionCode and versionName attributes on the manifest tag:

<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="__PACKAGE__" android:versionCode="220000" android:versionName="2.2.0">

For iOS, the version is specified in the Info.plist file. In a NativeScript project, you will typically find that file under the app/App_Resources/iOS directory. Look for the CFBundleShortVersionString and CFBundleVersion keys:

<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>CFBundleShortVersionString</key>
    <string>2.2.0</string>
    <key>CFBundleVersion</key>
    <string>2.2.0</string>
  </dict>
</plist>

We need to create a script that can look for the version generated by Standard Version in our package.json, updates these two files accordingly and adds everything to the version commit automatically.

To update the AndroidManifest.xml & Info.plist files, we need to install a few tools to manipulate XML and PList files:

$ npm install --save-dev xml-js xml-beautifier plist

Then create a standard-version.js file in the root directory of your project. We will use that file to open each file, update the version where appropriate and save the file back to the disk.

Now we can create a pre-release script to trigger this code, stage the files and update our release script to make ensure any staged files will be included in the version commit from Standard Version. Update your package.json like so:

"scripts": {
  "pre-release": "node standard-version.js && git add --all",
  "release": "standard-version -a"
}

Having the pre-release npm script allows us to test our standard-version script without generating a release commit and tag.

Finally, to run our pre-release script every time we run our release script, we have two options:

  1. Update the release script to run pre-release beforehand:
"scripts": {
  "pre-release": "node standard-version.js && git add --all",
  "release": "npm run pre-release && standard-version -a"
}
  1. Update our package.json with a Standard Version post-bump hook:
"standard-version": {
  "scripts": {
    "postbump": "npm run pre-release"
  }
}

Personally, I prefer the second option, as it will enforce the pre-release script even if I run Standard Version with the standalone standard-version command.

We can now push our new version to version control using:

$ git push --follow-tags

and our version is updated in all the right places automagically.