DEV Community

Cover image for Enforce Git Hooks in Monorepos with Husky - But How?
Miki Stanger
Miki Stanger

Posted on

Enforce Git Hooks in Monorepos with Husky - But How?

How to set up Husky hooks in a monorepo? Why should you? How to use them to enforce linting policies among your team?

It's another day of work. It's a good day at work - you finish stuff! You commit your changes and push them, and go to make yourself some caffeinated beverage while you wait for the CI to do its thing.
You come back and spill your beverage in shock as you see the CI failed! If only you could've prevented it!
You remember your friend talking about git hooks, and setting up something for themselves, but their idea for sharing was to send you a file. "It's hard to enforce hooks in a monorepo", they said.

After googling a bit, though, you find a nice article that explains how to do that (this one right here!). This is how it can be done:

Very TL;DR

Husky hook -> Main monorepo script with scope -> Individual package scripts

TL;DR

  • Set up Husky & hooks in your main monorepo directory
  • Your hooks should use your monorepo tool to run a script of a specific name by running it with scope.
  • Each package that you'd like to have this hook work on should have a script of that same name defined in its package.json file.

What's this about?

Git hooks are scripts that run on different stages of the git workflow. They can be used for side effect, or to fail a flow.

For example, the pre-commit hook runs when you try to commit something. If it fails (exits with code 1), the commit will not be made.
This is a perfect mechanism to enforce linting and tests before a developer's code is added to the repository.

However, there's one problem with this approach - git hooks are a git mechanism - they are local, user-specific and live in your project's .git folder. How can you enforce that nice pre-commit script for all users?

Husky is a wonderful package that allows you to define hooks for your package, then install them. But...

The Monorepo Problem

In monorepos, your repository is managed outside of your individual packages.

When you try to use husky with an individual package, it just won't work.

# Works okay
> npx husky add .husky/pre-commit "<something to execute>"

# Will not add your pre-commit hook to the right directory...
> npx husky install

# ...so it'll not run when it's supposed to :(
> git commit -m "Blah"
Enter fullscreen mode Exit fullscreen mode

The Solution

I'm using pnpm & pnpm workspaces for my project, so I'll use those for the code examples. They are easily interchangeable with your package manager & monorepo tools of choice.

Also, I'm demonstrating this with adding a pre-commit hook for linting. The same method should work with any other hook as well.

1. Install Husky in your main monorepo

Go to your main monorepo directory, and run:

pnpm i -D husky
Enter fullscreen mode Exit fullscreen mode

2. Set a main pre-commit script

In your main monorepo's package.json file, set a pre-commit script that runs a specific script in all projects.

Most (or all) monorepo tools have an option to run scripts for all, or some of the packages, by defining a scope.

In my current project, I make sure to use npm's scope notation (not related to the previous scope I mentioned) for all package names (@product/ui, @product/api etc.). This allows me to run a script on all packages easily:

> pnpm run --filter "@product/*" myscript
Enter fullscreen mode Exit fullscreen mode
// main package.json
{
  scripts: {
    "pre-commit": "pnpm run --filter \"@product/*\" pre-commit"
  }
}
Enter fullscreen mode Exit fullscreen mode

At this opportunity, you could also add a prepare script that runs husky install, so every user that sets up your project will have those hooks added to their .git directory automatically:

// main package.json
{
  scripts: {
    "pre-commit": "pnpm run --filter \"@product/*\" pre-commit",
    "prepare": "husky install"
  }
}
Enter fullscreen mode Exit fullscreen mode

3. Add a husky pre-commit hook:

Still in your main directory, create a husky pre-commit hook that runs your new pre-commit script:

> npx husky add .husky/pre-commit "pnpm pre-commit"
Enter fullscreen mode Exit fullscreen mode

4. Add pre-commit scripts for individual packages

The last things you'll have to add are the individual pre-commit scripts for each package.
I won't get into the actual linter configuration here, but I recommend using lint-staged with whatever linters you're already using. Lint-staged will only check staged files, so you'll save time and noise by only getting errors that are relevant to your current commit.

// Individual package's package.json
{
  scripts: {
    "pre-commit": "lint-staged"
  }
}
Enter fullscreen mode Exit fullscreen mode

If you don't add a pre-commit script to a project, nothing will break. This is good for many cases, but beware not to miss adding a hook script for a new project because of this.

5. And that's that!

The next time you'll commit something, all of your package's pre-commit scripts will run.

Conclusion

Git hooks & Husky are powerful tools that allow you to enforce running local scripts and keeping coding standards. By using this approach, you can enjoy the advantages of hooks and of proper script scoping (each project has its own script inside it) in a monorepo.

What do you think? Would you approach this differently?


Thanks to Yonatan Kra for his thoughtful and thorough review. Check out his blog for many good articles of an experienced developer.

The cover picture was made with DALL-E 2. I don't know a lot about fishing, so I'm trusting its judgement about how fishing rods look like.

Top comments (1)

Collapse
 
fruntend profile image
fruntend

Сongratulations 🥳! Your article hit the top posts for the week - dev.to/fruntend/top-10-posts-for-f...
Keep it up 👍