No More Global Npm Packages

The JavaScript development community has been welcoming new and powerful tools by the dozens each year, almost too fast for mortals to keep up with. Many of these tools come with npm packages that you can install globally so you can use the command line tool from anywhere on your computer. This can be very convenient, but is it the right way to do it? Is there a better alternative?

Is it Right?

Yes, it sure is… but no. Well, which is it!?!? Both. If you’re experimenting with a tool or just using it in general, feel free to use it as a global command line tool. If your project is actually dependent on the tool, then installing it globally probably isn’t the best way to go.

Why not? Well, just like pretty much every package on npm, the command line tools are versioned. If your local/development machine has a different version of the tool than another machine that needs to run that tool for that project, then there could be incompatibilities. It’d be nice if we had an option to save global dependencies inside of package.json, but honestly a package file shouldn’t include global utilities, as discussed in a GitHub issue for npm.

Grunt did a good job of minimizing this issue by creating a separate npm package for the cli tool that is extremely minimal so you can keep it installed globally with minimal chances of incompatibility. There still are no guarantees, though.

What’s the Alternative?

npm is the alternative. Specifically, you should include your development tools in devDependencies in package.json. That way they can be versioned for each project. But then how do you run it on the command line? Nobody wants to have to type in node ./node_modules/.bin/grunt every time they want to run a Grunt command. While that would work for the majority of tools, there is a slightly simpler way: npm scripts. You can use npm run WHATEVER to run any command you put into the scripts section of your package.json file, and if you reference a command that doesn’t exist on your computer’s PATH, then it’ll go searching through the node modules of your project for it.

For example, if you didn’t have the Grunt CLI installed globally, but had this in your package.json file:

1
2
3
4
5
6
7
8
9
10
11
12
...
devDependencies: {
"grunt-cli": "~0.1",
"grunt": "~0.4",
"grunt-contrib-jshint": "~0.6",
...
},
scripts {
"lint": "grunt lint",
...
},
...

… you could run npm run lint and it would run grunt lint correctly. Also, if you use npm v2 or later, your npm scripts can accept arguments:

1
2
3
4
5
6
...
scripts {
"grunt": "grunt",
...
},
...

Now you can run npm run grunt -- lint and it’ll run grunt lint. This new feature with version 2 allows us to use -- to pass arguments into the command that npm is running. You can also use this to guarantee that some options are always passed to your favorite commands. For example, you can make Grunt always run in verbose mode without the need to specify it each time:

1
2
3
4
5
6
...
scripts {
"grunt": "grunt -v",
...
},
...

You can run it the same way, but it’ll always be verbose now. This is a great way to make sure some options that you always want set are used without having to specify them every time.

I’ve run into a significant snag, though when doing this in Powershell on Windows. Assume we are still using the "grunt": "grunt" script setup. If I try typing npm run grunt -- -h in Powershell, then the -h argument will actually be sent to npm run instead of to the grunt command. Any arguments you try to pass in that start with - or -- will not be sent to the command in the scripts configuration, but to the npm run command. This doesn’t seem to be an issue with the standard cmd or Git Bash on Windows, nor does it look like Linux/Mac users are affected.

From countless comments I’ve read on the internet, it doesn’t sound like there are terribly many Powershell + npm users out there, but I very highly doubt that I’m the only one. In any case, this issue doesn’t come up all that often, since I tend to put all the commands I’m going to actually run into the scripts, so I rarely need to pass arguments and I tend to end up with nice aliases that actually tends to make it so that npm run COMMAND is shorter than the actual command it is running.

In fact, I’m looking around and trying to move beyond Grunt, Gulp, and the like, so I’m trying to use tools individually, instead of their task runner plugins (e.g. using the JSHint command line tool instead of the Grunt plugin). For more information, see “How to use npm as a Build Tool”, but I’m still experimenting with that and I’m certainly not trying to convince you of that just yet. This article is simply about removing the need for global npm packages.

Pros and Cons

There are certainly some pros and cons to this approach, so let’s take a look at some:

Cons

  • It usually takes more keystrokes thanks to needing to throw npm run in front of everything and needing to add -- in every time you want to throw some extra arguments in. Of course, this is negated when you use the scripts to convert long command names down to a short alias.
  • It takes a bit of extra work up front to add the scripts into the package.json file.

Pros

  • You have an easy place to find documentation on what command should be run. Just typing npm run by itself will show all the aliases and commands that are run for those aliases.
  • Depending on how you set up the npm scripts, they can act as an abstraction layer so that if you want to switch from Grunt to Gulp or something similar, you can just change what commands are being run inside the scripts configuration and never have to change the commands you actually type into the command line.
  • Obviously, one pro has to do with how I started this post: eliminating issues regarding version conflicts from global packages.
  • Finally, when someone clones your project, it increases the likeliness that all they will need to do is run npm install and they will actually get all of the dependencies installed.

Closing Questions

If I missed any pros or cons let me know. In any case, what do you think? Is it worth the fuss to prevent global package version conflicts? If you say yes, then how much do we rely on npm scripts versus letting other tools, like task runners do the real work? These are certainly questions you’ll have to answer for yourself or discuss with your team and not something I can just tell you the “right” answer to. I’d still love to hear your thoughts on using npm scripts to any extent, so let me hear it! God bless and happy coding!

Author: Joe Zimmerman

Author: Joe Zimmerman Joe Zimmerman has been doing web development ever since he found an HTML book on his dad's shelf when he was 12. Since then, JavaScript has grown in popularity and he has become passionate about it. He also loves to teach others though his blog and other popular blogs. When he's not writing code, he's spending time with his wife and children and leading them in God's Word.