Add Commitlint, Commitizen, Standard Version, and Husky to SvelteKit Project

Add Commitlint, Commitizen, Standard Version, and Husky to SvelteKit Project

ยท

6 min read

4/19/2022 Updates. I published a second post according to this one: Add lint-staged to SvelteKit Project

Recently I spent a lot of time improving our internal SvelteKit project by aligning coding style & repo quality, especially the commit message and versioning.

In this post, I will share how I added Conventional Commits and SemVer standards to our project and walk you through the setup of commitlint, commitlizen, standard-version, and husky.

What are Conventional Commits & SemVer?

Here, I copy the summary from each website. You can find more via the link.

Conventional Commits

The Conventional Commits specification is a lightweight convention on top of commit messages. It provides an easy set of rules for creating an explicit commit history, which makes it easier to write automated tools on top of. This convention dovetails with SemVer, by describing the features, fixes, and breaking changes made in commit messages.

The commit message should be structured as follows:

<type>[optional scope]: <description>
[optional body]
[optional footer(s)]

/* Example */
feat(landing): add register button and authentication

SemVer - Semantic Versioning

Given a version number MAJOR.MINOR.PATCH, increment the:

  1. MAJOR version when you make incompatible API changes

  2. MINOR version when you add functionality in a backward-compatible manner

  3. PATCH version when you make backward-compatible bug fixes

Additional labels for pre-release and build metadata are available as extensions to the MAJOR.MINOR.PATCH format.

How to adopt in your existing SvelteKit (or any other JavaScript) Project?

You can find the demo repo here: conventional-commits-sveltekit.

commitlint

First, install commitlint, a linting tool to check if your commit messages meet the conventional commit format:

# Install commitlint cli and conventional config
pnpm add -D @commitlint/config-conventional @commitlint/cli

# Configure commitlint to use conventional config
echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.cjs

Use .cjs instead of .js because "type": "module" is set in package.json in SvelteKit project.

Now let's test if commitlint works (no prompt if linting passed): pnpm commitlint.gif

๐Ÿถ husky

It's recommended to use husky with commitlint, a handy git hook helper, to check your commit message before making any git commit.

Will add more git hooks later.

# Install husky
pnpm add -D husky

# Active hooks
pnpx husky install

# Add hook
npx husky add .husky/commit-msg 'pnpx --no -- commitlint --edit $1'

Let's make a git commit: commitlint and husky.gif

husky would abort the commit if it didn't pass the commitlint

commitizen

It isn't very pleasant if we need to type the valid commit message every time.

commitizen is a CLI tool to help us fill out any required commit fields at commit time.

# Install commitizen, 
# @commitlint/cz-commitlint (adapter), and
# inquirer (peer dependency of @commitlint/cz-commitlint)
pnpm add -D commitizen @commitlint/cz-commitlint inquirer

Simply use pnpm cz instead of git commit when committing. Or add a pnpm script to your package.json:

...
  "scripts": {
    "commit": "cz"
  }

Let's give it a try: commitizen.gif

We use @commitlint/cz-commitlint as a commitizen adapter to ensure the commit adheres to the commit convention configured in commitlint.config.cjs.

standard-version

We've configured commitlint, husky, and commitizen to easily create and validate our commit message.

Now, let's see how we can automate versioning and changelog generation using standard-version.

# Install standard-version
pnpm add -D standard-version

Add pnpm script to your package.json.

...
  "scripts": {
    "release": "standard-version --no-verify" 
    // use --no-verify to skip git hooks we'll introduce later
  }

Here is how standard-version works:

  1. Retrieve the current version of your repository by looking at packageFiles, falling back to the last git tag.
  2. bump the version in bumpFiles based on your commits.
  3. Generates a changelog based on your commits (uses conventional-changelog under the hood).
  4. Creates a new commit including your bumpFiles and updated CHANGELOG.md.
  5. Creates a new tag with the new version number.

Let's try: standard-version.gif By default, CHANGELOG.md would only show commits belonging to feat or fix, but I like to have every commit in my changelog and add emojis.

We can customize it by creating .versionrc under your project root folder. Here is my setting:

{
  "types": [
    {
      "type": "feat",
      "section": "โœจ Features"
    },
    {
      "type": "fix",
      "section": "๐Ÿ› Bug Fixes"
    },
    {
      "type": "chore",
      "hidden": false,
      "section": "๐Ÿšš Chores"
    },
    {
      "type": "docs",
      "hidden": false,
      "section": "๐Ÿ“ Documentation"
    },
    {
      "type": "style",
      "hidden": false,
      "section": "๐Ÿ’„ Styling"
    },
    {
      "type": "refactor",
      "hidden": false,
      "section": "โ™ป๏ธ Code Refactoring"
    },
    {
      "type": "perf",
      "hidden": false,
      "section": "โšก๏ธ Performance Improvements"
    },
    {
      "type": "test",
      "hidden": false,
      "section": "โœ… Testing"
    }
  ]
}

You can see more options in Conventional Changelog Configuration Spec.

๐Ÿถ husky - more git hooks

Before we push commits and tags, we can consider adding more git hooks to improve our code quality. Like formatting, linting, and testing.

Maybe you've already set "Format On Save", run linting, or unit testing in watch mode, but it's a personal preference. To ensure a better code quality and efficient collaboration, we can add git hooks like pre-commit, and pre-push to format, lint, and test before opening a PR.

Below we'll take the SvelteKit skeleton project as an example. I choose Typescript, Prettier, ESLint, and Playwright.

First, add formatting and linting at pre-commit stage:

# Add pre-commit hook
npx husky add .husky/pre-commit 'pnpm format && pnpm lint && git add .'

pre-commit.gif Second (optional), add svelte-check at pre-push stage:

# Add pre-push hook
npx husky add .husky/pre-push 'pnpm check'

pre-push.gif

I didn't put playwright test here because I think it's better to do the e2e test in CI. If you're using a test runner like jest, uvu, or vitest, you may consider running unit/ component tests in the pre-commit or pre-push stage.

Caveats

git hooks is not a silver bullet. Your colleague can still "bypass" it in specific ways, but it'd be good to use and align with your team.

Some argue that "Run linting and testing in git hooks is just a drag on productivity and can't force anything. Why do this if we've already had them in CI? It's even useless if your tests didn't run in a production-like environment."

That might be true and depends on your team and your focus.

We use it anyway while thinking about the "Shift-Left" approach in DevOps.

Wrap up

Thank you for your reading! ๐Ÿ™Œ

We've walked through the steps to adopt Conventional Commits and SemVer in your project. Also, there are lots of different commitizen adapters and commitlint configurations you can try.

I tried gitmoji and followed Make everyone in your project write beautiful commit messages using commitlint and commitizen by Sohan Dutta, but end up using the setting in this article because the prior is not compatible to changelog generation of standard-version (Ref: Issue #859)

My team and I are still working on our first CI/ CD pipeline, and this article is our first step to improve our workflow.

Please kindly share your thoughts and experience. Love to learn from you!

You can find me on Twitter: @davipon

Useful resources:

Continuous Delivery by Dave Farley (Full of wisdom)

Master the Code Review by Curtis Einsmann (I haven't finished yet, but the content is so great that I need to share it here!)

ย