<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Eduard Mavliutov</title>
    <description>The latest articles on DEV Community by Eduard Mavliutov (@eduardmavliutov).</description>
    <link>https://dev.to/eduardmavliutov</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F617205%2Fbaa421df-8427-420c-9afa-a79a5b6ca890.jpeg</url>
      <title>DEV Community: Eduard Mavliutov</title>
      <link>https://dev.to/eduardmavliutov</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/eduardmavliutov"/>
    <language>en</language>
    <item>
      <title>Set up git hook in a multi-package monorepo</title>
      <dc:creator>Eduard Mavliutov</dc:creator>
      <pubDate>Mon, 24 Feb 2025 09:45:05 +0000</pubDate>
      <link>https://dev.to/eduardmavliutov/set-up-git-hook-in-a-multi-package-monorepo-3kg7</link>
      <guid>https://dev.to/eduardmavliutov/set-up-git-hook-in-a-multi-package-monorepo-3kg7</guid>
      <description>&lt;h2&gt;
  
  
  What’s a git hook and how we can manage them?
&lt;/h2&gt;

&lt;p&gt;Let’s assume that you already know what git hook is, where they must be placed and how they can be managed. In case you don’t I would try to fill you in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Git hook&lt;/strong&gt; is a script that’s run when we perform specific git operations in our repository. Here’s a list of some git hooks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;pre-merge-commit;&lt;/li&gt;
&lt;li&gt;prepare-commit-msg;&lt;/li&gt;
&lt;li&gt;commit-msg;&lt;/li&gt;
&lt;li&gt;post-commit. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full list of hooks can be found &lt;a href="https://git-scm.com/docs/githooks" rel="noopener noreferrer"&gt;here&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Git hooks must be placed&lt;/strong&gt; in your repository in &lt;em&gt;.git/hooks&lt;/em&gt; folder. So, if you want to set up, e.g., a pre-commit hook, you should create &lt;em&gt;pre-commit&lt;/em&gt; file in that folder. &lt;/p&gt;

&lt;p&gt;For instance, I might have a pre-commit git hook with the following script: &lt;code&gt;echo "My favourite band is Red Hot Chili Peppers"&lt;/code&gt;. And once I commit it's going to be triggered:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcca7fzeu0b8xeauntsk0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcca7fzeu0b8xeauntsk0.png" alt="Pre-commit hook is triggered!" width="800" height="121"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There are many libraries that help developers &lt;strong&gt;manage git hooks&lt;/strong&gt;: &lt;a href="https://github.com/typicode/husky" rel="noopener noreferrer"&gt;Husky&lt;/a&gt;, &lt;a href="https://github.com/toplenboren/simple-git-hooks" rel="noopener noreferrer"&gt;Simple git hooks&lt;/a&gt;, &lt;a href="https://github.com/evilmartians/lefthook" rel="noopener noreferrer"&gt;Lefthook&lt;/a&gt;, etc..&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Recently I needed to set up a pre-commit hook to lint staged files within the following git repository:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Git repo contains of 100+ Node.js apps, the repository itself is not an app.&lt;/li&gt;
&lt;li&gt;Each project already contained its own eslint config, given different code owners all existing configs must be preserved. &lt;/li&gt;
&lt;li&gt;Files must be linted even if changes are made to multiple projects.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I bet you got the idea, but here's how its structure looked like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;project/
 src/
  project-1/
   package.json
  project-2/
   package.json
  project-3/
   package.json
...
.gitingnore
README.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I am not going to create 100 of demo apps just to show you my pain, a couple of demo projects should be enough, I believe.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/eduardmavliutov/monorepo-pre-commit/tree/start" rel="noopener noreferrer"&gt;This is our starting point&lt;/a&gt;, the project now looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;project/
 src/
  demo-project-1/
   package.json
  demo-project-2/
   package.json
.gitingnore
README.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To solve the problem we are going to use: &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;em&gt;npm workspaces&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;eslint&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;lint-staged&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;husky&lt;/em&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let’s get job done!&lt;/p&gt;

&lt;h3&gt;
  
  
  Npm workspaces
&lt;/h3&gt;

&lt;p&gt;According to &lt;a href="https://docs.npmjs.com/cli/v8/using-npm/workspaces" rel="noopener noreferrer"&gt;documentation&lt;/a&gt;: &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Workspaces is a generic term that refers to the set of features in the npm cli that provides support to managing multiple packages from your local file system from within a singular top-level, root package.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Given that I want to have all listed packages installed only once, having workspaces set up in repository’s root with &lt;em&gt;eslint&lt;/em&gt;, &lt;em&gt;lint-staged&lt;/em&gt; &amp;amp; &lt;em&gt;husky&lt;/em&gt; as dev dependencies would do the job.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In order to quickly create &lt;em&gt;package.json&lt;/em&gt; run &lt;code&gt;npm init&lt;/code&gt; in the root.&lt;/li&gt;
&lt;li&gt;Add &lt;em&gt;workspaces&lt;/em&gt; property to the newly created &lt;em&gt;package.json&lt;/em&gt; file, saying that all apps inside the &lt;em&gt;src&lt;/em&gt; folder are now parts of workspaces.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;At this point the file looks as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "name": "monorepo-with-pre-commit-git-hook",
  "version": "1.0.0",
  "main": "index.js",
  "workspaces": ["src/*"],
  "author": "eduard mavliutov",
  "license": "ISC",
  "description": ""
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inner packages won’t even notice that each of them now is a part of workspaces.&lt;/p&gt;

&lt;h3&gt;
  
  
  Eslint
&lt;/h3&gt;

&lt;p&gt;Let’s ensure all packages use the same version of &lt;em&gt;eslint&lt;/em&gt; by installing &lt;em&gt;eslint&lt;/em&gt; &amp;amp; &lt;em&gt;eslint-config-airbnb-base&lt;/em&gt; dependencies in the root &lt;em&gt;package.json&lt;/em&gt;. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fglhf6xlhr3wyoue3whzv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fglhf6xlhr3wyoue3whzv.png" alt="_package.json_ in the root" width="800" height="482"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6ldpgh6uq9eyvo43tn6y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6ldpgh6uq9eyvo43tn6y.png" alt="demo-project-1's _package.json_" width="800" height="611"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgrt8tjjqrfp1zeiuo4k6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgrt8tjjqrfp1zeiuo4k6.png" alt="demo-project-1's _package.json_" width="800" height="596"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Lint-staged
&lt;/h3&gt;

&lt;p&gt;To lint staged files we’re going to use the &lt;a href="https://github.com/lint-staged/lint-staged" rel="noopener noreferrer"&gt;lint-staged&lt;/a&gt; package: it’s easy to configure and supports different configurations per each project. &lt;/p&gt;

&lt;p&gt;We are going to install it in the root project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install --save-dev lint-staged
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ok, now let’s configure it for linting js files. There are &lt;a href="https://github.com/lint-staged/lint-staged?tab=readme-ov-file#configuration" rel="noopener noreferrer"&gt;multiple ways&lt;/a&gt; to do that, you can check it out yourself. We will go with &lt;em&gt;&lt;a href="https://github.com/lint-staged/lint-staged?tab=readme-ov-file#how-to-use-lint-staged-in-a-multi-package-monorepo" rel="noopener noreferrer"&gt;lintstagedrc.json&lt;/a&gt;&lt;/em&gt; file per project. That option suits our needs the most: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;lintstagedrc.json&lt;/em&gt; can be updated separately per project;&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;lintstagedrc.json&lt;/em&gt; can have different sets of tasks per project.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For demo purposes each project will use the same config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "**/*.{js,ts}": [
    "eslint --fix"
  ]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To run the package we need to use &lt;code&gt;npx lint-staged&lt;/code&gt; command, we’re going to use the command in the pre-commit hook, but we can give it a go now:&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/Ngz8Jep1FeI"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h3&gt;
  
  
  Husky 🐶
&lt;/h3&gt;

&lt;p&gt;Again, we’re going to install it in the root project.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install --save-dev husky
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we need 2 more things:&lt;br&gt;
1) Create &lt;em&gt;pre-commit&lt;/em&gt; file with &lt;code&gt;npx lint-staged&lt;/code&gt; command in it. We're going to use these commands in order to do that.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mkdir .husky
echo "npx lint-staged" &amp;gt; ./.husky/pre-commit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;2) Create &lt;em&gt;prepare&lt;/em&gt; command in &lt;em&gt;package.json&lt;/em&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"scripts": {
  "prepare": "husky"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ok, we installed &lt;em&gt;eslint&lt;/em&gt;, &lt;em&gt;lint-staged&lt;/em&gt; and &lt;em&gt;husky&lt;/em&gt; in the root project. Let’s clean up a bit to ensure that necessary dependencies are installed only on the top level. I am going to add a couple of new scripts right after the &lt;em&gt;prepare&lt;/em&gt; one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"scripts": {
  "prepare": "husky”,
  "clean": "bash -c \"rm -rf node_modules &amp;amp;&amp;amp; rm -rf ./src/*/node_modules\"",
  "reinstall": "npm run clean &amp;amp;&amp;amp; npm install"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The final &lt;em&gt;package.json&lt;/em&gt; looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "name": "monorepo-with-pre-commit-git-hook",
  "version": "1.0.0",
  "main": "index.js",
  "workspaces": [
    "src/*"
  ],
  "scripts": {
    "prepare": "husky",
    "clean": "bash -c \"rm -rf node_modules &amp;amp;&amp;amp; rm -rf ./src/*/node_modules\"",
    "reinstall": "npm run clean &amp;amp;&amp;amp; npm install"
  },
  "devDependencies": {
    "eslint": "8.52.0",
    "eslint-config-airbnb-base": "15.0.0",
    "husky": "^9.1.7",
    "lint-staged": "15.4.3"
  },
  "author": "eduard mavliutov",
  "license": "ISC",
  "description": ""
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the &lt;em&gt;reinstall&lt;/em&gt; command and we’re ready! Now let’s get mischievous!😈&lt;/p&gt;

&lt;h3&gt;
  
  
  Showtime
&lt;/h3&gt;

&lt;p&gt;I am going to break some eslint rules in &lt;em&gt;/src/demo-project-1/index.js&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg60d8vrc3wlt47xu0km4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg60d8vrc3wlt47xu0km4.png" alt="index.js file in demo-project-1 with eslint errors" width="800" height="378"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;and in &lt;em&gt;/src/demo-project-2/index.js&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz5b6bjulg0ecvbkzn3fm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz5b6bjulg0ecvbkzn3fm.png" alt="index.js file in demo-project-2 with eslint errors" width="800" height="434"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And now let’s try to commit that. For the sake of demo I am going to open my terminal in &lt;em&gt;demo-project-1&lt;/em&gt;’s folder.&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/SrJe0cDKhDw"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Thus, we have one &lt;em&gt;eslint&lt;/em&gt; error in one of files and a couple of warnings in another, I tried to commit that and it didn’t work. Cool! &lt;/p&gt;

&lt;p&gt;Let’s fix the error that’s blocking the commit and leave fixing the rest to &lt;em&gt;eslint&lt;/em&gt;:&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/QpOeBOidles"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Yay! &lt;/p&gt;

&lt;h2&gt;
  
  
  Summing up
&lt;/h2&gt;

&lt;p&gt;Here's what we have in the end:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Our projects remain independent even if each of them is a part of workspaces.&lt;/li&gt;
&lt;li&gt;In each project staged code is linted in the pre-commit git hook.&lt;/li&gt;
&lt;li&gt;If developer works on multiple projects at once, all staged files are linted no matter the project.&lt;/li&gt;
&lt;li&gt;Each project may have its own &lt;em&gt;eslint&lt;/em&gt; config OR all projects may use one &lt;em&gt;eslint&lt;/em&gt; config.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://github.com/eduardmavliutov/monorepo-pre-commit" rel="noopener noreferrer"&gt;Git&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Photo in header by &lt;a href="https://unsplash.com/@max_duz?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash" rel="noopener noreferrer"&gt;Max Duzij&lt;/a&gt; on &lt;a href="https://unsplash.com/photos/man-facing-three-computer-monitors-while-sitting-qAjJk-un3BI?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;

</description>
      <category>monorepo</category>
      <category>git</category>
      <category>webdev</category>
      <category>husky</category>
    </item>
  </channel>
</rss>
