UPDATE: I released a npm package for this script: git-pull-run. Please report any issues or suggestions to improve on GitHub.
Together with my team I've been working on one project repository with multiple packages - a monorepo. Of course, we're using Git and branches, so there is almost no friction between us. Except when it comes to dependencies - in our case npm dependencies - but I guess it also holds true for other environments. When I pull the latest changes on my current branch or I switch between different branches, I have to be aware if the package-lock.json
(lock file) has been changed. If so, I have to run npm install
to make sure my dependencies are up to date with the latest changes. Otherwise, I might run into hard-to-find bugs where the current development works on some machine but not on the other due to outdated dependencies.
Hooks To The Rescue
We're already using a pre-commit hook to automatically run linting and formatting on git commit
. That's quite easy with Git Hooks and a tool like Husky. Fortunately, Git also supports a post-merge hook that runs after a git pull
is done on a local repository. This is exactly the point in time where we need to update our dependencies to see if they have changed. For detailed steps on how to get started with hooks, I recommend following this guide.
Detect Changes
When we git pull
the latest changes, we need a list of all changed files. If this list contains a package-lock.json
, we need to run npm install
to update our dependencies. If we working on a monorepo with multiple packages as in my case, we need to run it for each changed package. The following git diff
prints the list of changed files:
git diff --name-only HEAD@{1} HEAD
With a simple regular expression, we can filter all paths containing a package-lock.json
file. I did put the regex into the PACKAGE_LOCK_REGEX
variable, because this part must be changed depending on the actual project structure. It contains a matching group (i.e. the first pair of parentheses) that starts with packages/
, because in our monorepo all packages live under this directory (except for development dependencies which live at project root directory). The result of regex filter is saved as array into the PACKAGES
variable.
IFS=$'\n'
PACKAGE_LOCK_REGEX="(^packages\/.*\/package-lock\.json)|(^package-lock\.json)"
PACKAGES=("$(git diff --name-only HEAD@{1} HEAD | grep -E "$PACKAGE_LOCK_REGEX")")
Run Install
Finally, we need to run npm install
for each changed package. As Git runs on the project root directory and the changed files are actually paths to lock files, we must change the directory before running install. With $(dirname package)
we can easily extract the directories from the path.
if [[ ${PACKAGES[@]} ]]; then
for package in $PACKAGES; do
echo "📦 $package was changed. Running npm install to update your dependencies..."
DIR=$(dirname package)
cd "$DIR" && npm install
done
fi
Post Merge Hook
All the above snippets can be combined into the following shell script, that is going to be executed by Husky as post-merge hook.
The file must be saved as post-merge
(no .sh extension) inside the .husky
folder. I'm running on macOS with zsh as default shell (see shebang #!/bin/zsh
) and it's working. However, I didn't test it with bash, so there might be some changes necessary if you run a different shell.
Test It
In order to verify if the hook is working, we can reset the current local branch to a previous state (e.g. rewind 20 commits) and then pull the changes back.
git reset --hard HEAD~20 && git pull
If the package-lock.json
has been changed in one of the commits, the hook will print a nice little message for each lock file and it will automatically run npm install
for us. If you use Git-integration of Editors like VSCode, you need to check the output of the Git log in order see what's going on.
Top comments (15)
Nice one. I am looking for solutions on how to improve:
Your opinions, suggestions would be highly appreciated.
Can't we just run npm install on each pull
You mean always running install without checking for lock file changes? Of course that’s an option. I’m using npm’s
postinstall
lifecycle hook to set up the dev environment (generate code, deploy changes, etc.). So avoiding unnecessary installs is important to me.This is pretty cool, can it be a package one can just install?
I’m working on a npm package :)
I released a first version: npmjs.com/package/git-pull-run
I'd like to hear your thoughts! :-)
Shouldn't you run
npm ci
instead ofnpm install
?Won't
npm install
try to update your package.json?If the semver versions in
package.json
andpackage-lock.json
fit with each other,npm install
should behave identically tonpm ci
.For more information check this comment by the creator of
npm ci
: dev.to/zkat/comment/epbjHi there, nice article!
Do we need to setup this husky hook into package.json?
No, Husky in the latest version v7 doesn't work npm scripts anymore. Just follow these steps and it create a .husky folder in your repo. Then add a post-merge file with gist snippet above.
Say goodbay to ‘npm install’, no need ‘npm install’, throw away ‘npm install’.
Good point! Another way we are solving it at work is by using yarn pnp with zero installs. It's not bulletproof but surely helps a lot.
I’d like to use yarn pnp but it’s not widely supported yet. For example Typescript doesn’t support pnp at the moment (it’s being patched into TS by yarn during install). Also bundlers like webpack didn’t handle the lock file at root level correctly the last time I checked.
If we are using yarn then How to do same thing ?
That should be easy with changing the regex expression from package-lock.json to yarn.lock:
PACKAGE_LOCK_REGEX="(^packages\/.*\/yarn\.lock)|(^yarn\.lock)"
and runningyarn install
instead ofnpm install
inside the hook.