Automatically resolve NPM package-lock conflicts using Git

Ever wondered how to get Git to automatically resolve conflicts on NPM package-lock.json files? You’re not alone.

Why store package locks in Git?

While an NPM package.json file allows you to specify dependencies using semver ranges, it’s still possible that there are many different combinations of package versions that satisfy these ranges.

This can be problematic especially when multiple people are working on a project, because pulling in the latest version of a repo and running an npm install might suddenly cause the build to break for that developer, because of a bug or other incompatibility introduced in a (slightly) newer version of one of the dependencies.

“But it worked on my machine!”

your co-worker

So, to ensure everyone on a project is using the exact same versions of certain packages, package managers like NPM, Yarn or PNPM nowadays create so-called package lock files (typically named package-lock.json, yarn.lock or pnpm-lock.json). They contain a list of all packages and their dependencies, and their exact versions.

Because such a lock file is (should be) committed to Git, your co-worker will end up using the exact same set of packages that you were using during your development.

Merge conflicts

Package locks are auto-generated files, and auto-generated files should typically not be committed in version control. One of the reasons is that they can and often will cause merge conflicts.

In this case, they contain lots and lots of package versions, and it’s easy for one small package update to cause a cascade of other ‘version bumps’.

These bumps will likely collide and cause merge conflicts when pulling in latest changes, especially when you’re working with feature branches (e.g. Git Flow).

By default, Git will try to merge the package lock file, and basically leave you with a big mess full of conflict markers. Things can be improved a little bit by telling Git it should treat the file as a ‘blob’ during a merge, e.g. by creating a file called .gitattributes in your repository:

package-lock.json merge=binary

This will tell Git to not try to merge it, but instead just keep your local version. However, it will still leave the package-lock.json file marked as having conflicts.

Auto-merge theirs

There are two issues with this:

  1. The merge is aborted, leaving package-lock.json with conflicts.
  2. The default choice was to keep your local version.

The latter is not ideal, because it’s likely that you pulled in changes from e.g. master or integration, and that version is supposed to contain a well-tested combination of package versions, possible as a result of combining various feature branches.

A manual way to fix this, is to run git checkout --theirs package-lock.json.

This will take upstream’s version as the basis, and remove the conflict state. You can then simply run npm install to regenerate it, which will (re-)include any recent changes you have made to your package.json. Nice.

Extra nice would be if we can tell Git to basically do this --theirs trick itself, and it turns out we can.

Instead of just telling Git to treat the lock file as one blob (merge=binary), we can tell it to use a specific merge driver to perform the merge. So let’s define a driver called theirs, which will simply always use changes from the ‘other branch’ (%B), and copy it over our local file (%A) (see “Defining a custom merge driver” on the Git attributes docs).

You need to run the following commands once on each developer machine:

git config --global "Keep changes of upstream branch"
git config --global merge.theirs.driver "cp -f '%B' '%A'"

(This will make the driver available to all of your repos. To just use it on one, remove --global and run it in that repo.)

Then, add the following to your .gitattributes:

package-lock.json merge=theirs

Done! Whenever you now pull changes, any conflicts on the package lock will automatically be ‘resolved’.

Just run npm install like you normally would (due to the upstream changes), to regenerate package-lock.json to include your local changes.

Did this post help you? Please leave a comment below!

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.