author: Krasimir Tsonev

Krasimir is a blogger, who writes and speaks.
He loves open source and codes awesome stuff.

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.

blog comments powered by Disqus