func0der
func0der

Reputation: 2232

npm global installation of package.json does not link binaries

my setup is the following:

{
  "name": "gadada",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "me",
  "dependencies": {
    "bower": "latest",
  },
  "devDependencies": {
  }
}

Since I can not properly install those modules in the web directory, because there is a known issue with the synched folders and npm, I want to install them globally. No harm done, because it is a dedicated virtual machine for one and only one web app.

So I do:

sudo npm install -g

Afterwards I try to use bower, which isn't working, because it did not get linked during npm install.

It works fine when I do sudo npm install -g bower@latest

What am I doing wrong?

Upvotes: 1

Views: 1277

Answers (2)

func0der
func0der

Reputation: 2232

Okay, I found out, that npm does not work that way. You can not install dependency command line tools globally from a package.json. Thank you for pointing that out, @Matthew Bakaitis

I dug through the npm code yesterday and found a piece of code that explains this behavior:

var linkStuff = build.linkStuff = function (pkg, folder, global, didRB, cb) {
  // allow to opt out of linking binaries.
  if (npm.config.get('bin-links') === false) return cb()

  // if it's global, and folder is in {prefix}/node_modules,
  // then bins are in {prefix}/bin
  // otherwise, then bins are in folder/../.bin
  var parent = pkg.name && pkg.name[0] === '@' ? path.dirname(path.dirname(folder)) : path.dirname(folder)
  var gnm = global && npm.globalDir
  var gtop = parent === gnm

  log.info('linkStuff', packageId(pkg))
  log.silly('linkStuff', packageId(pkg), 'has', parent, 'as its parent node_modules')
  if (global) log.silly('linkStuff', packageId(pkg), 'is part of a global install')
  if (gnm) log.silly('linkStuff', packageId(pkg), 'is installed into a global node_modules')
  if (gtop) log.silly('linkStuff', packageId(pkg), 'is installed into the top-level global node_modules')

  shouldWarn(pkg, folder, global, function () {
    asyncMap(
      [linkBins, linkMans, !didRB && rebuildBundles],
      function (fn, cb) {
        if (!fn) return cb()
        log.verbose(fn.name, packageId(pkg))
        fn(pkg, folder, parent, gtop, cb)
      },
      cb
    )
  })
}

As found here: https://github.com/npm/npm/blob/master/lib/build.js#L93

As you can see here, the comment about the parent variableclearly points out, that only packages installed in the "root" node modules folder will be linked globally and all others will be linked in .bin of the packages folder.

To clarify that behavior, I going to give you quick example. So the structure after I ran npm install -g would be:

/usr/lib/node_modules
| --> npm
| --> gadada

As we can see, npm has created a new subfolder call gadada which is the name I have given in my package json. In there the dependencies placed and links are created within /usr/lib/node_modules/gadada/.bin. This is because when the depdencies are built, npm check which parent folder they have. If it is not /usr/lib/node_modules, it would not get linked globally.

For comparison, the outcome of me running npm install -g bower@latest now, would produce this folder structure:

/usr/lib/node_modules
| --> npm
| --> gadada
| --> bower

At installation npm would realize the parent folder of bower is /usr/lib/node_modules and links globally.

This is the whole voodoo behind it. I have not found any site explaining this particular behavior, but I guess it is to prevent dependencies messing with global installations of binaries.

Just for clarification: You global folder for node modules can differ. /usr/ is just my prefix.

I tried to solve this problem by using `scripts. I found a great tutorial about it, but lost it due to private browsing :/

I suggested to include the commands you need in the scripts section of your package.json like this:

{
  "name": "gadada",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "bower": "bower"
  },
  "author": "me",
  "dependencies": {
    "bower": "latest",
  },
  "devDependencies": {
  }
}

Before you scream: Nooo, what the hell, the full path is missing, consider this. When you run a command from the scripts with npm run bower, npm includes the .bin folder of your $PATH, so that the binaries can be found. Magic at its finest :)

I hope I could help some people who stumble across the same problem :)

Upvotes: 3

Matthew Bakaitis
Matthew Bakaitis

Reputation: 11990

If you mean you are trying to install the dependencies in package.json globally with npm install -g...that's not how it works. package.json is specifically and only used to define locally installed packages. It's the manifest of required modules that can ride along with a project, allowing the total project size to be smaller in transit (i.e. you don't have to include node_modules in your repo, etc).

The -g flag is for individually installed global packages, typically for things like command line utilities. While your situation wouldn't really have any serious issues if every module in the project were installed globally, in other cases such a situation would be a Bad Thing.

So you aren't doing anything wrong, it's just not how npm works for installs.

However, you can use the npm scripts to define a pre-install or post-install step that would essentially run a -g install. This is extra work, but if you own the project and you're going to be doing this alot...it could be a nice way to automate things.

Upvotes: 0

Related Questions