Enforcing monorepo dependency consistency with Sherif

This lesson preview is part of the Bundling and Automation in Monorepos course and can be unlocked immediately with a \newline Pro subscription or a single-time purchase. Already have access to this course? Log in here.

This video is available to students only
Unlock This Course

Get unlimited access to Bundling and Automation in Monorepos, plus 90+ \newline books, guides and courses with the \newline Pro subscription.

Thumbnail for the \newline course Bundling and Automation in Monorepos
  • [00:00 - 00:18] In this lesson, we're going to introduce Sherif, an opinionated zero configuration linter for JavaScript and TypeScript Monorepos. Its job is to help keep all package versions aligned, and detect any inconsistencies that can crop up in a large codebase.

    [00:19 - 00:34] The best way to see what this means is to just install it and run it. I'm going to install it in the root of our monorepo with "pnpm add -w (which means workspace root) -D (which means a dev dependency) sherif".

    [00:35 - 00:48] I'm then going to open my package.json and create a "sherif" script. I'm going to run it with "pnpm sherif", and it's going to let us know that we have some issues in our example Monorepo.

    [00:49 - 01:21] Because I ran this command ahead of time, I know that there is an issue that's being hidden, just because my terminal is at a very large size for screencasting purposes, I'm going to show only the first error by doing "pnpm sherif | head -16", which means only show the first 16 lines of output. So the first issue reported by Sherif is that our root package.json contains both a "dependencies" array and a "devDependencies" array.

    [01:22 - 01:42] In a Monorepo, your root package.json should always only use "devDependencies". The reason for that is that dev dependencies are not going to be installed when you run pnpm production installs, and that matters for things like docker install that we might look at later in the course.

    [01:43 - 02:08] It's also just a good general rule of thumb to have any tooling be dev dependencies, and by definition, in our Monorepo root, we would never have code that's going to ship into production. So any dependency in the root is a dev dependency. I'm going to quickly fix this by moving all dependencies into the "devDependencies" array, deleting the "dependencies" array and then sorting our dependencies.

    [02:09 - 02:53] Then I'm going to run "pnpm install" so our change takes effect and then I'm going to run "pnpm sherif" again. Now the errors that we see on screen are all errors currently detected by Sherif. If we look at the errors now, Sherif has found that we have the same packages installed, a different versions in different parts of our Monorepo. We don't want that because maintaining a single shared version of the same package across the Monorepo helps avoid unpredictable bugs and also keeps the installed size smaller. To fix this, I'm going to use Sherif's interactive fix option by running "pnpm sherif -f".

    [02:54 - 03:27] Now Sherif is going to show me each different package and the versions that this package is installed at, and ask me which version do I want to make the only shared version across the whole Monorepo. For almost all packages, we're just going to select the highest version that's currently installed, and that's going to work fine. The one package that I'm not going to select a version for is ESLint, because I know that actually ESLint eight and ESLint nine are not compatible.

    [03:28 - 03:40] And if I forced ESLint nine everywhere, it's going to lead to breakage. For now, I'm just going to skip it with escape, let Sherif finish. It's going to install the changed versions.

    [03:41 - 04:22] I'm going to run it one more time and we're going to see that the only error left is the ESLint error. To work around Sherif complaining about different versions, when this is our intent, I'm going to use an approach we saw earlier. I'm going to go into the apps/frontend-nextjs/package.json file, and for the ESLint dependency I'm going to use the protocol syntax specifying "npm:eslint@^8" instead of simply the version number, and Sherif is going to skip validating this dependency.

    [04:23 - 04:38] Sherif does not validate dependencies that use protocol specification, and this way we can have ESLint at version 8 here, and also have Sherif not complain about it. I'm going to now rerun "pnpm sherif -f".

    [04:39 - 04:54] It's going to ask us about the other two packages, the server-express one and the frontend-vite that use ESLint. I'm going to tell it to use the latest version of ESLint there, and then it's going to install those versions. But we've run into an issue.

    [04:55 - 05:17] I have apparently made a typo and instead of writing "eslint", I wrote "eslit". So let me quickly fix that and run "pnpm install" again, and now everything works correctly. I'm going to go to the changes by doing "git add -p", which means show me patches for each change.

    [05:18 - 05:45] We can see that for frontend-nextjs, we have set the React version to be a more exact version number, and we have also set TypeScript to be an exact version number, and we have set ESLint to use the protocol syntax. The next change is in frontend-vite, where we have specified now the latest versions of ESLint and TypeScript. In server-express, we have upgraded to the latest version of TypeScript.

    [05:46 - 06:03] And finally, in the root package.json, we have added the "sherif" command, moved our dependencies to dev dependencies and we have added "sherif" as a dependency. I'm going to commit this as "Add Sherif command and fix issues".

    [06:04 - 06:23] Finally, we want to add Sherif to our lefthook.yaml file so it runs automatically before each commit. This way, if anyone introduces inconsistent dependency versions, we catch it right away. I'm going to call this check "lint monorepo with sherif", and it's just going to run the Sherif binary.

    [06:24 - 07:07] I'm going to add all changes and then commit this with "Add Sherif to pre-commit checks". And now when we look at Lefthook output we can see our "lint monorepo with sherif" run with no issues. And because I forgot to do this last time when we added a new check to Lefthook, let me quickly run hyperfine and see how long does Sherif take, and we can see that it's extremely fast at 28 milliseconds, so I highly recommend for any project to always have this enabled as a pre-commit check. With this, our Monorepo is now protected from the common pitfall of mismatched dependency versions.

    [07:08 - 07:18] Sherif enforces this consistency. It keeps our installation smaller and ensures more predictable behavior across all our workspaces.