Detect when node_modules are out of sync
I’m pretty sure this already happened to you as well. You pull down the latest main
branch or jump to some feature branch and you start getting weird errors. Until only later you recognize you forgot to execute an npm install
. Let’s look into how we can avoid that.
Actually, if you’re using Webstorm you might be on the safe side becaus it’ll show you a nice little notification whenever it detects that node_modules
are out of date. But not everyone uses Webstorm. (Personally big VSCode fan but currently kinda jumping back and forth between the two).
So, Emma just tweeted about exactly this problem, which reminded me of a script.
{{< twitter 1298916262559576065 >}}
The Strategy
The strategy is basically to check whether the package.json
changed between the current and previous Git head.
This could be expressed with the following git command
$ git diff-tree -r --name-only --no-commit-id <previous-head> <current-head>
As an example. If I jump from my main
branch (or master
) to my my-cool-feature
branch, I could execute
$ git diff-tree -r --name-only --no-commit-id main HEAD
.gitlab-ci.yml
angular.json
apps/myapp/myapp-e2e/src/app.e2e-spec.ts
apps/myapp/myapp-e2e/src/stackblitz.e2e-spec.ts
apps/myapp/myapp-e2e/tsconfig.json
libs/myapp/util/src/lib/stackblitz/stackblitz-filelist.ts
libs/myapp/util/src/lib/stackblitz/stackblitz-writer.ts
nx.json
package-lock.json
package.json
As you can see we get a list of files which we can now parse for the appearance of package.json
.
Implementing the script
So let’s implement it as a Node.js script.
Exec the Git command from Node
First step is to execute our git
command, which we can do with shelljs.
#!/usr/bin/env node
const shell = require('shelljs');
// we'll get these via the command line args
const [
NODE_PATH,
SCRIPT_PATH,
PREVIOUS_HEAD,
CURRENT_HEAD,
ISBRANCH,
] = process.argv;
// get a list of change files as a string
let changedFiles = shell.exec(
'git diff-tree -r --name-only --no-commit-id ' +
PREVIOUS_HEAD +
' ' +
CURRENT_HEAD
);
shelljs
has a method exec(...)
that allows to directly issue the git command and get the output as a string. Note we get PREVIOUS_HEAD
and CURRENT_HEAD
which are needed for the git
command to work properly. More about that later.
Check for the presence of package.json
Once we have the changed files, we can verify whether it contains package.json
...
if (changedFiles.includes('package.json')) {
// print to the user that he/she should exec an npm install
// (or even just do the npm install automatically)
}
The entire script
To make it a bit nicer, add some colors. I place the script in some tools
folder within my repo.
Here’s the entire script:
// tools/node-modules-check.js
#!/usr/bin/env node
const shell = require('shelljs');
const colors = require('colors');
const fs = require('fs');
const [
NODE_PATH,
SCRIPT_PATH,
PREVIOUS_HEAD,
CURRENT_HEAD,
ISBRANCH,
] = process.argv;
let changedFiles = shell.exec(
'git diff-tree -r --name-only --no-commit-id ' +
PREVIOUS_HEAD +
' ' +
CURRENT_HEAD
);
if (changedFiles.includes('package.json')) {
let msg = 'package.json changed: ';
// personalize it based on whether the user uses
// yarn or npm
if (fs.existsSync('yarn.lock')) {
msg += 'Please run "yarn install"';
} else {
msg += 'Please run "npm install"';
}
// some message coloring & design ;)
let width = 80;
console.log(
colors.bold.inverse.yellow(
[
'='.repeat(width),
' '.repeat(width),
msg.padStart(msg.length + (width - msg.length) / 2).padEnd(width, ' '),
' '.repeat(width),
'='.repeat(width),
].join('\n')
)
);
}
Installing the script as a Git Hook
The easiest way to install Git hooks is Husky. Follow the setup instructions in their README. Most commonly you simply add some special nodes in your package.json
, like
// package.json
{
"husky": {
"hooks": {
"pre-commit": "npm test",
"pre-push": "npm test",
"...": "..."
}
}
}
or you create a .huskyrc
file, which I did for our example here:
hooks:
"post-checkout": "cross-env-shell node tools/node-modules-check.js $HUSKY_GIT_PARAMS"
I’m using the post-checkout
hook, which means it runs every time you checkout something, whether it is a new branch, a new commit etc. Which is exactly what we want, right?
Conclusion
See the script in action on this GitHub repo. Clone the repo and switch between it’s master
and feature/cowsay
branch.