Juri Strumpflohner

RSS

Detect when node_modules are out of sync

Author profile pic
Juri Strumpflohner
Published

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.