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.