Global dependencies: they're insecure and they harm your contributors
Mar 28, 2016
Global dependencies are everywhere: task managers like Grunt and gulp, test runners such as Karma and ava, transpilers like CoffeeScript and TypeScript.
Having globally installed modules which are available from the command line can speed things up immensely when you're working. Being able to use browserify to bundle your modules, or docco to generate documentation for your code can save heaps of time and effort.
Your contributors, however, may not appreciate them as much as you do.
What's the difference between global and not-global?
A global dependency is stored irrelevant to any specific project. It is generally accessed with a command, for example tsc
to access the TypeScript compiler. This means that it can be easily placed in npm scripts, for example test, because you can do "test": "mocha test.js"
.
A local dependency is specified in the project, and is stored per project. The version is specified in the package.json
file which ensures consistent behaviour throughout contributors. It does, however, mean that you can't globally access it on the command line with a quick command like tsc
or gulp
.
How are they insecure?
When you add a dependency locally to your project, you have the power to control the version that people are using with it.
You can state that you only want to accept a single version of the dependency with "some-dependency": "1.2.3"
. You can also specify that you want to accept any version which has a matching minor version with "another-dependency": "~0.3.1"
. If you want to accept any version with a matching major version, you can do "one-more-dependency": "^0.8.6"
, or if you want to accept any version of the dependency (please don't ever do this), you can do "bad-dependency": "*"
.
When you ask for a user to have a globally installed dependency, you can place no restrictions on the version of the dependency. This means that they could have newer, or older versions with different functionality to what is expected in your codebase.
Read more about the npm versioning system in "Semver: Tilde and Caret" on NodeSource.
Where's the harm?
A typical TypeScript web app will definitely rely on a few modules which are usually globally installed. There's TypeScript for a start, and then probably browserify. If there are unit tests around the project then they might be executed with Karma. To use their chosen testing framework in TypeScript, they'll either need TSD or Typings installed. Lastly there might be a tool like istanbul to look at their code coverage and a tool to generate documentation such as docco.
If a contributor is looking at helping out with the project and they haven't worked with these technologies before, then this is a lot of work just to get it up and running.
$ git clone ...
$ npm install -g typescript
$ npm install -g browserify
$ npm install -g karma
$ npm install -g typings
$ npm install -g istanbul
$ npm install -g docco
$ npm install
$ # now we can do things!
That's a lot of commands that need to be executed in order for the contributor to start being able to contribute. Six global dependencies isn't the highest I've ever seen, either.
Why do people use global dependencies?
The main reason for packages being installed globally is that most of the README
's for well-used libraries call for them to be installed globally.
TypeScript
For the latest stable version:
npm install -g typescript
browserify
With npm do:
npm install -g browserify
typings
Quick Start
npm install typings --global # Install Typings CLI utility.
istanbul
Getting started
$ npm install -g istanbul
docco
Installation:
sudo npm install -g docco
Of all the dependencies in the example above, the only one that didn't explicitly tell you to install it globally was karma (go karma!)
So when a contributor hasn't experienced those modules before, they'll likely head to the npm page for them. They'll come across the "Installation" / "Quick Start" / "Introduction", see that they should use the -g
tag and will do so.
If only there were a way to transfer this effort away from the contributors...
The Solution
Just because a module prefers global installation, that doesn't mean that it requires it. All global installation does is provide a shortcut to the module that you can access with a single command.
Take TypeScript, for example. Your npm script might look like this:
"build": "tsc -p src"
Which, if you ask for global installation of TypeScript (tsc
), only works if the user has installed TypeScript (tsc
) globally - we can't guarantee this from their npm install
. The solution is to install TypeScript as a developer dependency. The maintainer of the project needs to run npm install typescript --save-dev
, which will install TypeScript within the context of the project and then save it as a developer dependency.
You don't need to change your npm script
entry - npm will evaluate tsc
as a valid command because it is installed in the project (and will even prefer the project's version over the globally installed version!)
The example from earlier is no longer a behemoth with 8 different commands, it's down to this:
$ git clone ...
$ npm install
$ # now we can do things!
Your contributors are then able to type these two commands and get on with what they wanted to do: contribute to your project.
tl;dr
- They're insecure: You can't guarantee anything about a global dependency, as you are unable to specify the version.
- They're extra steps: Relying on global dependencies adds a lot of extra steps for contributors when they want to get to work on your repo.
- Save them into your project: Don't rely on global dependencies - install them with the
--save-dev
flag and commit that to your project. - Your scripts stay the same!:
npm
will automatically resolve the commands to the locally installed dependencies!tsc
is valid in npm scripts when you have TypeScript as a dependency.