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
oryarn install
(or justyarn
).
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:
1 |
npm i[nstall] {package} -g |
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“:
1 |
#!/usr/bin/env node |
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:
1 |
yarn global add {package} |
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:
1 |
yarn global bin |
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:
1 |
PATH=/usr/local/Cellar/node/7.7.3/bin:$PATH |
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:
1 |
yarn global add create-react-app --prefix /usr/local |
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:
1 |
yarn global remove create-react-app --prefix /usr/local |
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:
1 |
yarn config set prefix /usr/local/ |
Now, when you run, for example:
1 |
yarn global add create-react-app |
…that package will be installed into ~/.config/yarn/global/node_modules/
and will be symlinked in /usr/local/bin
. And if you later run:
1 |
yarn global remove create-react-app |
…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!
Discover more from Gavin Orland
Subscribe to get the latest posts sent to your email.
Armand says
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.
Vijay Patil says
Perhaps this tool could help:
https://www.npmjs.com/package/fix-yarn-global-packages
Gavin says
Yes, could do. Will have to have a closer look at that.