DEV Community

Cover image for Semantic versioning in JavaScript projects made easy
Stijn Van Asschodt
Stijn Van Asschodt

Posted on

Semantic versioning in JavaScript projects made easy

If you've used a package manager like npm or yarn before, you're probably familiar with a versioning format like X.Y.Z, where X, Y, and Z each represent a number, separated by dots. But what do those numbers mean?

This versioning format is called Semantic Versioning (or SemVer for short). Those three numbers correspond to: <MAJOR>.<MINOR>.<PATCH>. Updating the major version means introducing a breaking change, the minor version is incremented when adding a new feature and the patch version is increased when including backward-compatible bug fixes. Increasing the version number (often called "bumping") also requires an update of the project's changelog. However, managing this manually for every release seems like a tedious task. After all, a developer most likely prefers writing code over documentation. Luckily, there are some tools to help automate this!

🛠 Tools

Standard version is a utility that takes care of all these versioning steps. It bumps the version, writes the changes to the changelog, and creates a git tag with the new version. It requires conventional commit messages when committing, meaning all commit messages should follow a specific pattern:

<type>[optional scope]: <description>

[optional body]

[optional footer]

The fix: and feat: types correlate to the PATCH and MINOR version respectively. Adding a BREAKING CHANGE: prefix to the body or footer of the commit message indicates a bump of the MAJOR version.

But how can you make sure contributors stick to this format, to prevent standard version from breaking?
Similar to how a linter like eslint can be used to analyze your code, a tool like commitlint can be used to analyze your commit messages. By adding commitlint as a commit-msg git hook, all commit messages can be evaluated against a predefined config, ahead of the actual commit. So if the linter throws an error, the commit fails. An easy way to create those git hooks, is by using a helper like husky, which allows you to define your hooks directly inside the package.json.

Additionally, using an interactive CLI like commitizen, simplifies writing the commit messages in the conventional commit format by asking questions about your changes and using your answers to structure the message.

💿 Setup

Install all the necessary tools.

npm install --save-dev standard-version commitizen @commitlint/{cli,config-conventional} husky

Create a commitlint.config.js file in the root of the project. This file defines the rules that all commit messages should follow. By extending the conventional commit config, created by the commitlint team, all conventional commit rules will be added to the config.

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

Configure the hook in the package.json.

{
  ...
  "husky": {
    "hooks": {
      "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
    }
  }
}

A commit not following the conventional commit pattern, will now fail and give appropriate feedback regarding what caused the error:

$git commit -m "non-conventional commit"
husky > commit-msg (node v10.15.3)
⧗   input: non-conventional commit
✖   subject may not be empty [subject-empty]
✖   type may not be empty [type-empty]

✖   found 2 problems, 0 warnings
ⓘ   Get help: https://github.com/conventional-changelog/commitlint/#what-is-commitlint

husky > commit-msg hook failed (add --no-verify to bypass)

Next, initialize the conventional changelog adapter to make the repo commitizen-friendly:

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

Add 2 scripts to the package.json: one to run the commitizen cli and one for standard-version:

{
  ...
  "scripts": {
    "cm": "git-cz",
    "release": "standard-version"
  }
}

💻 Usage

Now, when using npm run cm to commit, commitizen's cli will be shown. It asks a series of questions about the changes you're committing and builds the message based on the provided answers. For example, committing a new feature looks like this:

commitizen cli example

When everything is ready for a new release, use standard-version to update the version number, changelog and create the git tag:

npm run release

Standard version's output shows the bumping of the minor version to 1.1.0, as expected when committing a feature, and that a correct git tag was created.

✔ bumping version in package.json from 1.0.0 to 1.1.0
✔ outputting changes to CHANGELOG.md
✔ committing package.json and CHANGELOG.md
husky > commit-msg (node v10.15.3)

✔ tagging release v1.1.0
ℹ Run `git push --follow-tags origin master && npm publish` to publish

The outputted changes to the CHANGELOG.md look like this:

# Changelog

All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.

## 1.1.0 (2020-04-13)


### Features

* short desc, added to changelog ([cd9dbc9](https://github.com/Hzunax/semantic-versioning-example/commit/cd9dbc9627b7fc64ba0490e495fd71686a604e57))

Each fix, feat, or BREAKING CHANGE commit will show up in the changelog with its short description and a link to the commit on the remote.

Standard version also takes care of committing these changes (with a conventional commit message), so all that's left to do is push the changes to the remote and we're done!

📖 Further reading

I made an example setup where I use the tools described in this post. Feel free to check out the commit messages and how they are represented in the changelog.

For more complex configurations and more detailed information on the tools and concepts used in this post, check out the links below.

Top comments (1)

Collapse
 
pdina profile image
Paolo E Basta

Awesome, thank you very much for this, I was able to setup version management as I intended in a couple of hours.

I'd like to add just this, if you need/want to change the generated changelog, adding for example chore commit messages, you can create a .versionrc file and add something like:

{
  "types": [
    {"type":"feat","section":"Features"},
    {"type":"fix","section":"Bug Fixes"},
    {"type":"chore","section":"Chores"},
    {"type":"test","section":"Tests", "hidden": true},
    {"type":"build","section":"Build System", "hidden": true},
    {"type":"ci","hidden":true}
  ]
}
Enter fullscreen mode Exit fullscreen mode