Maybe don’t globally-install that Node.js package

Jake Wilson
codeburst

--

How many times have you checked out a Node.js package and the instructions tell you to install it globally?

# npm install some-cool-package -g

Is it a bad idea to install it globally? Does it matter? What are the advantages and disadvantages? What is the best alternative?

That’s what we are going to figure out.

The globally-installed Gulp.js example

Like many, my first real exposure to Node was using Gulp to automate compiling of SASS to CSS. Installing Gulp was straightforward. Install it locally in your project and also install it globally:

# npm install gulp --save-dev
# npm install gulp -g

You install Gulp globally so that you could run the # gulp command on your project. Back in 2013, it seemed odd to me to need to install something “twice” to make it work… but I didn’t know much about Node.js so I just accepted it and moved on. Over the next several years I ran into several packages that “needed” to be installed globally and eventually learned more details about this practice.

Is it bad to globally-install a package?

I wouldn’t necessarily say it’s “bad” to globally-install a package, but I don’t think it’s a good idea. Personally, I hate globally-installing things. I feel like I am polluting my OS or something… Or adding bloat… It’s hard to explain. Some things obviously you need to globally-install, but just because a package tells you to globally-install it, is that your only option?

Beyond my personal feelings, globally-installing a package that your projects depend on is bad for two primary reasons:

Global vs Local version mismatching

I’m going to use Gulp as an example again, but this can apply to many packages. If you install Gulp globally and also locally for a project, this works great because both version of Gulp match. But what happens later when you work on a different project and are forced to update your global Gulp to match the project’s Gulp? What happens if you download an older project that uses an even older version of Gulp? Now your global Gulp version is different than the local Gulp used in multiple projects. There is a good chance that these version mismatches are eventually going to conflict and cause problems. Your global # gulp command is going to try to do something that a project’s local Gulp is incompatible with (or vice-versa). That’s an obvious problem.

Project Self-Containment

Let’s say you want to contribute to an Open Source project. You download the code and go through the deployment instructions, which require you to globally-install a bunch of packages. In my opinion that not right. A project should be completely self-contained and self-sufficient. It shouldn’t require a bunch of external, globally-installed dependencies to make it work. One should be able to download project code and later delete that project from their computer without a bunch of residual globally-installed packages left over, hidden away somewhere. The project itself should contain everything necessary to make it function.

Now, this doesn’t mean that a project shouldn’t depend on external services like a database or 3rd-party API. I’m only referring to Node dependencies for Node projects.

What is the alternative solution?

We’ve established that globally-installing project dependencies can cause technical problems and probably isn’t a best-practice. So what do we do?

Anyone who uses Node has seen the node_modules directory that is created when you install dependencies for a project. If you examine what goes on in node_modules you’ll see that this is where NPM installs all the packages and their dependencies.

If a locally installed package has an executable script, the executable is somewhere in the node_modules directory. For example to execute gulp at the command-line in your project directory, you simply need to execute this:

# node ./node_modules/gulp/bin/gulp.js
or
# node_modules/gulp/bin/gulp.js

So you don’t even need the globally-installed gulp now…

However, typing all this isn’t very convenient. How can we clean this up?

node_modules/.bin

Have you ever noticed the node_modules/.bin directory? NPM does this cool thing, where if a locally installed package has an executable script in it, the package can add a symlink in the .bin directory. For example:

node_modules/.bin/gulp → ../gulp/bin/gulp.js

So now you can execute this:

# node_modules/.bin/gulp

That’s an improvement… but we can do better:

NPM scripts

NPM has this great feature where you can specify scripts to be run in your project’s package manifest. So for example we could add this to our package.json:

...
"scripts": {
"gulp": "./node_modules/.bin/gulp"
},
...

Our command is now even more simple:

# npm run gulp

NPM manipulates the PATH env variable

NPM also has a slightly hidden feature where it adds node_modules/.bin to the front of your PATH environment variable whenever npm run script is executed. So your package.json entry can be simplified to this:

...
"scripts": {
"gulp": "gulp"
},
...

This is documented here and here.

NPM run’s scripts in subdirectories too

A lot of executable Node scripts, like Gulp, Grunt or Knex.js require your terminal to be at the project root directory in order to execute them. A great feature of using the NPM run script approach is that NPM will execute your scripts if you are in a subdirectory too. You can navigate into any subdirectory into your project and execute # npm run gulp and NPM will go up your directory tree until it finds your package.json and run the command just as expected.

What packages does this apply to?

I’ve used Gulp in a lot of examples in this article, but what other packages does this apply to? The answer is pretty much any package that wants you to run their command(s) in your terminal. Some examples come to mind:

gulp, Grunt, webpack, Node-sass, Coffeescript, eslint, Typescript, Knex, uglify-js, browserify, nodemon, etc…

Every single one of these scripts can be executed from your project’s node_modules/.bin directory without issue. You do not need to install them globally.

Are there exceptions?

Yup. There are definitely exceptions to this. The packages listed above are all packages that projects (typically) use directly. In other words, projects depend on those packages to run properly.

But there are plenty of Node.js packages that are not used directly within projects that it makes sense to globally-install. Some examples:

npm (yes npm is just a Node script that is globally-installed), bower, express-generator, Yeoman, PM2, Forever, etc…

The common theme is that these are Node scripts that are not used from within projects, but rather are used to create, setup, scaffold or manage execution of projects. It makes total sense for these types of Node scripts to be installed globally because when someone else wishes to install your project code, these scripts are not required dependencies that they need to install.

gulp-cli

As I have used Gulp in my examples, you may be quick to point out that Gulp eventually changed their recommended installation procedure to NOT globally-install itself but rather to install a specific cli version:

# npm install gulp --save-dev
# npm install gulp-cli -g

The gulp-cli package still gives you the # gulp command in your terminal, except all it’s really doing is looking into your project’s local node_modules directory and executing that local version of Gulp. So gulp is no longer really globally-installed. Grunt does this as well. The reason they do this is specifically for the global-vs-local version mismatching problem that I already mentioned.

It’s great that Gulp and Grunt both offer this solution, but:

  1. It’s still another globally-installed package that needs to be installed to work with any given project.
  2. It’s pointless since you can just use NPM scripts to do the same thing without having to globally-install anything.
  3. Not every package has a separate cli version of their script available to account for this. So you will definitely have version mismatch issues eventually with other Node packages.

So even though gulp-cli and grunt-cli exist, my recommendation is to bypass them and just use NPM scripts for everything. I’ve used Gulp as my primary example in this article simply because so many people have been exposed to it.

Regarding npx

In mid 2017, npm released a package called npx. This package gets invisibly installed globally whenever you install npm. npx is a global command-line tool that will allow you to execute scripts without having to globally install them. It works very similar to our npm run-scripts setup I have already described. The idea is that you can navigate to your project directory and run something like

# npx gulp

And the gulp that gets executed is the one located in ./node_modules/.bin. npx automatically looks in there for your executable and if it doesn’t find it, it does some other fancy stuff like looking in caches and determining if it needs to be installed, etc.

This is a great tool and is really helpful, but I typically still lean toward using npm scripts because you can include command-line arguments in your package.json. For example, let’s say you had some command with some arguments that you wanted to run with npx. It could look something like this:

# npx myDataModule -path ./some/dir/file.txt -output ./log/data.log

This is something that needs to be fully documented so that others using your package understand how to use the command and what arguments need to be passed to it. If you were instead using npm scripts, you could simply add these arguments to your package.json:

...
"scripts": {
"loadData": "myDataModule -path ./some/dir/file.sql -output ./log/data.log"
},
...

Now, you can simply run # npm run loadData. The command itself documents the exact command-line parameters you need. It get’s committed to your code base and no matter who uses your package, there is no confusion on how to “load your data” or whatever you are doing with your commands.

So yes, npx is very cool and helpful. But I personally still lean toward keeping all my essential package-related commands in npm scripts.

Conclusion

We’ve established that globally-installing Node packages is not really a good idea most of the time. Sure, there are exceptions to this when you look at package managers and project scaffolding tools.

Can you get away with globally-installing everything? You might be able to. You might never run into any sort of global-vs-local version mismatch issue. But the more Open Source projects you contribute to, the more likely it is that eventually you’ll have this problem. Every project under the sun is using some different version of some package than the next project. A version mismatch problem is inevitable.

In my opinion, globally-installing project dependencies is a bad practice in software development. Maybe there are situations where it’s unavoidable… but in your Node projects, aim for self-containment of dependencies as much as you can. If a contributor can download your code and simply execute

# npm install

and start coding right away, I think that is pretty great :-)

--

--

Full stack developer. Husband and father to three. I’m a total nerd.