A New git-hooks Package
Tags: npm, typescript, tutorial, git
Published: 2022-10-21 10:57:28

In [a previous post](https://dev.to/mbarzeev/a-git-hook-for-commit-messages-validation-no-husky-just-js-1hni) I found a way to use native Git hooks mixed with JS, to create a Git hook for verifying conventional commit messages. The end result was JS code, performing the validation logic inside in a “commit-msg” hook, and it was 🔥, but it immediately brought 2 thoughts to mind:

**One** - This has the potential to grow and by “grow” I mean that now, when it is so intuitive and easy to create a hook in JS, other hooks might follow (running lints, tests, etc.).
**Two** - The code cries out for packaging and sharing it as a git-hooks package, so that others can install it, import the required hook and execute it in the relevant Git hook.

***

My way of approaching such a task is to first visualize how using the package will be like in the end. Say that our package name will be “@pedalboard/git-hooks” (given that it is a part of my [pedalboard monorepo](https://github.com/mbarzeev/pedalboard)), I would like it to be used like this:

```bash
#!/usr/bin/env node
 
const {conventionalCommitsValidationHook} = require('@pedalboard/git-hooks');
conventionalCommitsValidationHook.execute();
```

In the example above, the user will require the “conventionalCommitsValidationHook” module from the “@pedalboard/git-hooks” package, and then call the “execute” method on it, which is the interface I plan for any hook in this package to implement.

I will create the “@pedalboard/git-hooks” package scaffold in the project first. You can read more about creating a package under a Monorepo in [previous articles I wrote on the subject](https://dev.to/mbarzeev/5-resources-to-get-your-monorepo-off-the-ground-3mlp), but for now just imagine that we have another package under the “packages” directory, with the relevant configuration, ready to be published.

Next thing is creating the `GitHook` interface which has a single method to it, “execute”, in a `GitHook.ts` file:

```javascript
export default interface GitHook {
   execute: () => void;
}
```

This interface should be implemented by any hook that this package will export. Let’s create our first hook, which will be the “conventional commits validation hook” - 
Within the `packages/git-hooks/src/conventional-commits-validation-hook` directory, here are the tests for this hook:

```javascript
import fs from 'fs';
import conventionalCommitsValidationHook from './index';
 
describe('conventional-commits-validation-hook', () => {
   const mockConsoleLog = jest.spyOn(console, 'log').mockImplementation((msg) => msg);
   const mockConsoleError = jest.spyOn(console, 'error').mockImplementation((msg) => msg);
   const mockExit = jest.spyOn(process, 'exit').mockImplementation((code) => code as never);
 
   beforeEach(() => {
       jest.clearAllMocks();
   });
 
   it('should have an execute method', () => {
       expect(conventionalCommitsValidationHook.execute).toBeDefined();
       expect(typeof conventionalCommitsValidationHook.execute).toEqual('function');
   });
 
   it('should exit on unexpected error', () => {
       conventionalCommitsValidationHook.execute();
       expect(mockConsoleError).toHaveBeenCalled();
       expect(mockExit).toHaveBeenCalledWith(1);
   });
 
   it('should be valid when for conventional commits', () => {
       const messages: Array<string> = [
           'feat: This is a mock message',
           'feat(shopping cart): This is a mock message',
           'fix: This is a mock message',
           'build(release): This is a mock message',
           'refactor(some comment): This is a mock message',
           'style: This is a mock message',
           'chore!: drop support for Node 6',
           'docs: correct spelling of CHANGELOG',
           'chore(release): publish',
       ];
 
       messages.forEach((msg) => {
           jest.clearAllMocks();
           jest.spyOn(fs, 'readFileSync').mockImplementation(() => msg);
           conventionalCommitsValidationHook.execute();
           expect(mockExit).toHaveBeenCalledWith(0);
       });
   });
 
   it('should fail validation when not a conventional commit', () => {
       jest.spyOn(fs, 'readFileSync').mockImplementation(() => 'not a conventional commit');
       conventionalCommitsValidationHook.execute();
       expect(mockExit).toHaveBeenCalledWith(1);
       expect(mockConsoleLog).toHaveBeenCalledWith(
           'Cannot commit: the commit message does not comply with conventional commits standards.'
       );
   });
});
 
```

I’m testing that several different conventional commits are accepted, while another one does not.
Here is the actual hook’s implementation:

```javascript
import fs from 'fs';
import GitHook from '../GitHook';
 
const conventionalCommitsValidationHook: GitHook = {
   execute: () => {
       try {
           const conventionalCommitMessageRegExp: RegExp =
               /^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test){1}(\([\s\w\-\.]+\))?(!)?: ([\w ])+([\s\S]*)/g;
           let exitCode = 0;
           const commitMsgFile = process.argv[2];
           const message: string = fs.readFileSync(commitMsgFile, 'utf8');
           const isValid: boolean = conventionalCommitMessageRegExp.test(message);
 
           if (!isValid) {
               console.log('Cannot commit: the commit message does not comply with conventional commits standards.');
               exitCode = 1;
           }
 
           process.exit(exitCode);
       } catch (error) {
           console.error(`Cannot commit: unexpected error occurred: ${error.message}`);
           process.exit(1);
       }
   },
};
 
export default conventionalCommitsValidationHook;
```

And this is how the package looks like in the monorepo. You can also check the [code on GitHub](https://github.com/mbarzeev/pedalboard/tree/master/packages/git-hooks):

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vhdq6kt9gi94xfktnckx.png)

Running the tests, I make sure it all passes and the coverage is sufficient before it’s time to build and use the package.

Inside my project’s `.git-hooks` directory, I have a “commit-msg” Git hook, [remember](https://dev.to/mbarzeev/a-git-hook-for-commit-messages-validation-no-husky-just-js-1hni)?
I start by installing the package on the project root - 

`yarn add @pedalboard/git-hooks -D` 

And then I refactor the code of the commit-msg hook to use the hook from the git-hooks package, like so:

```bash
#!/usr/bin/env node
 
const {conventionalCommitsValidationHook} = require('@pedalboard/git-hooks');
conventionalCommitsValidationHook.execute();
 
```

And… this is exactly how I planned it to be. Sweet! 
Trying to commit something with a non-valid commit message and I get the expected error. We are done :)

The “@pedalboard/git-hooks” package is already available on NPM so feel free to try it out and let me know what you think -

[@pedalboard/git-hooks](https://www.npmjs.com/package/@pedalboard/git-hooks 
)

As always, if you have any questions or comments, please leave them in the comments below so that we can all learn from it.

*Hey! If you liked what you've just read check out <a href="https://twitter.com/mattibarzeev?ref_src=twsrc%5Etfw" class="twitter-follow-button" data-show-count="false">@mattibarzeev</a><script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> on Twitter* :beers:

<small><small><small>Photo by <a href="https://unsplash.com/@kadh?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Kira auf der Heide</a> on <a href="https://unsplash.com/s/photos/package-delivery?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a></small></small></small>