author: Krasimir Tsonev

Hi there, I'm . Senior front-end engineer with over 13 years of experience. I write, speak and occasionally code stuff. Follow me on Twitter, GitHub, Facebook or LinkedIn

Fun playing with npm, dependencies and postinstall script

I like npm and the fact that I can install tons of stuff. It’s great piece of software and helps me solve problems everyday. Yesterday I had to use a postinstall script and hit a problem.

The problem

Let’s say that we have a module A that depends on module B. We have a package.json file like the one below:

{
  "name": "A"
  "version": "0.1.2",
  "dependencies": {
    "B": "0.1.2"
  }
}

When we run npm install we will get the following files/folder structure:

โ”œโ”€โ”€ node_modules
โ”‚   โ””โ”€โ”€ B
โ”œโ”€โ”€ package.json
โ””โ”€โ”€ README.md

Now, let’s say that both A and B depend on another C module. And not only that, they depend on same version of the module. Now it gets interesting. npm is smart enough to find out that C module should be installed on only one place and used equally by A and B. So it does the following:

โ”œโ”€โ”€ node_modules
โ”‚   โ”œโ”€โ”€ B
โ”‚   โ”‚   โ”œโ”€โ”€ node_modules
โ”‚   โ”‚   โ””โ”€โ”€ package.json
โ”‚   โ””โ”€โ”€ C   
โ”œโ”€โ”€ package.json
โ””โ”€โ”€ README.md

Everything seems ok. The C module is installed at the same level of B but B still has an access to it through require('C'). I believe that npm first checks in the local node_modules directory, then goes up and in the end checks the globally installed packages (not sure if that’s the exact order).

My problem is that I have a postinstall script in node_modules/B/package.json that uses the C module. Something like this:

{
  "name": "B"
  "version": "0.1.2",
  "dependencies": {
    "C": "0.0.1"
  },
  "scripts": {
    "postinstall": "node ./node_modules/C make"
  }
}

And of course after the installation the B module does not have C installed locally. It’s in the upper directory.

The solution

The first thing that I tried is accessing the C module from a script and not directly like in the package.json above. I created a file `runMe.js` in the `B`'s directory:

// node_modules/B/runMe.js
var C = require('C');
C.make();

And I replaced

"postinstall": "node ./node_modules/C make"

with

"postinstall": "node ./runMe.js"

That doesn’t work because the C module was not installed even in the upper directory when npm runs node ./runMe.js. We needed to wait a bit. In the end I just write the most hacky code for the day:

// node_modules/B/runMe.js
var deps = ['C'], index = 0;
(function doWeHaveAllDeps() {
  if(index === deps.length) {
    var C = require('C');
    C.make();
    return;
  } else if(isModuleExists(deps[index])) {
    index += 1;
    doWeHaveAllDeps();
  } else {
    setTimeout(doWeHaveAllDeps, 500);
  }
})();

function isModuleExists( name ) {
  try { return !!require.resolve(name); }
  catch(e) { return false }
}

I described all the needed dependencies in an array and simply wait till they are accessible via require.resolve. Dummy but it worked.

If you enjoy this post, share it on Twitter, Facebook or LinkedIn. Or maybe comment below: