An experienced Senior Front End Web Developer

Installing global NPM packages with Yarn

It is arguably best to install as many NPM packages as you can locally rather than globally. A couple of reasons for this are:

  • You then have specific versions of a given packages (or at least semver rules) for each project.
  • A fellow developer can be sure to be able to install all dependencies simply by running npm install or yarn install (or just yarn).

Some packages, though, are intended to be installed globally so that you can easily access their binaries because they are in your PATH variable. The PATH variable is essentially a list of locations which your shell will check for binaries.

In the case of NPM, we install packages globally using:

What happens is the module’s package is installed into /usr/local/lib/node_modules  and a symlink pointing to its binary is installed in usr/local/bin. This latter location is probably already in your PATH, so you can then type the name of that binary and it’ll be found and will run, no matter which directory you’re in.

Whoa! That “binary” is just a JavaScript file. How come I don’t need to prefix it with node when I run it, as I usually have to?

That’s a good question. If you examine these “binaries”, you’ll see they have something like this at the top – a “shebang“:

This is kind of a big deal. The file can tell the shell which other binary it should use to run it, so you don’t need to use specify it. That’s why it works.

So far so good, but what about Yarn?

With Yarn we add a package globally using:

Where is that package put, and where is the symlink to its binary put?

This is where we get into territory that is perhaps not as well documented as we might like. Global packages are installed by Yarn into ~/.config/yarn/global/node_modules/. Symlinks to the binaries of those packages appear to be installed into the current node version’s bin directory, where the node binary itself is located. You can actually run the following command which will confirm where those symlinks are being installed:

If you installed Node with its installer and the binary itself was put straight into your /usr/local/bin folder, things are going to work fine: that location is in your PATH and sought by the shell, so the symlinks to your global package binaries will also be found there and executed as expected.

But if you installed Node using Homebrew (and I recommended installing any system software via Homebrew where possible) then it’s a different story. Homebrew keeps previous versions of software in its “Cellar” and it inserts a symlink to the version to run (usually the latest) into your usr/local/bin instead. Neat.

The symlink is found and it runs Node which might actually be located in, for example, /usr/local/Cellar/node/7.7.3. That’s where the node binary is located and that’s where Yarn is going to install your global packages. Now we have a problem: this location is not checked by the system and your global packages are not going to be found!

One way around this would be to add that path to our PATH variable in our ~/.profile file which runs when we open a terminal:

This should work (when you open a new terminal). Our binaries are now found.

It doesn’t seem a great solution though, does it? It seems like we still have an architectural/organisational issue here: Yarn is installing symlinks to global binaries in a particular version of Node’s folder. What happens when we upgrade Node and Homebrew instead points its symlink to new folder /usr/local/Cellar/node/7.7.4?

Looks like we’ve got to remove all our global modules and install them all over again, or possibly move the symlinks. And we’ve got to go and update our PATH variable again. What, further, if we make Homebrew do a cleanup and it actually deletes the previous Node folder and all of our symlinks? Hm. Looks like a bit of a mess.

Luckily (and this is where we might like the Yarn documentation to be a little more thorough than it is) we can actually specify to Yarn where it should put the symlinks to the binaries of the global packages it installs. We do it using the --prefix flag:

Okay so it’s now going to put the symlink to create-react-app in /usr/local, right? Nope! It’s going to put it in the bin folder in there. Good stuff – problem solved. But beware, when you come to remove a global package that has been installed with this flag, the symlink will remain unless you again send in the flag:

But do we really want to have to specify this prefix flag every time we install a package globally anyway? No, not really. We can set the location where Yarn will install global symlinks permanently using:

Now, when you run, for example:

…that package will be installed into ~/.config/yarn/global/node_modules/ and will be symlinked in /usr/local/bin. And if you later run:

…the package will be removed from ~/.config/yarn/global/node_modules/ and the symlink will be removed from /usr/local/bin and you don’t need to specify a flag.

Looks like we have lift-off!

Space shuttle Atlantis blasts off at Kennedy Space Center

3 comments so far:

  1. this global install prefix logic is seriously flawed, and I don’t see a fix. The problem is that you have a system path pointing to a user install. This should never be allowed. the correct solution is what npm does with their prefix, which is to put the binaries into a system path, not a user path. This way other system users can access the files, and to ensure system isolation for enterprises where the home directory is on an NFS mount. Unfortunately, I have not found the solution in yarn for this just yet.

Comment on this: