Npm globals as local dependencies

Written by tacomanator | Published 2018/01/21
Tech Story Tags: javascript | npm | npm-global-modules | modules | software-development

TLDRvia the TL;DR App

For convenience, many npm based development tools instruct users to install globally. It makes sense if the tool is used to initiate/create a project, but many such modules are also used with an existing projects. These should installed as local (project) dependencies, which have several advantages:

  • The version in use is managed alongside other dependencies, keeping the team on the same page. This is especially important for build tools, used to produce a release. Following this practice could improve the reproducibility of builds, which in turn makes deployment and troubleshooting easier and more reliable.
  • The number of steps needed for new developers to get up and running is reduced, as all of the necessary tools are installed along with the other project dependencies.
  • Multiple versions of a module can be used on the same computer, for example when working on multiple projects which use the tool but may not be upgraded at the same time.

A small barrier to this practice is that local dependencies are not directly executable as they binaries live within the project folder. One fix is to add the projects node_modules/.bin folder to PATH, but don’t do this. For one, it would need to be done by every developer needing to use the command.

Another option is to add a script to package.json to act as a runner. For example, to make exp available as a command in your project, install as a project dependency (using --dev)and add script entry to act as a runner for your module:

# in package.json:"scripts": {"exp": "exp"...}

The exp module will now be installed along with other modules during the installation step, and, thanks to argument forwarding developers can run any subcommand using npm:

$ npm run exp -- build:ios$ npm run exp -- build:status$ npm run exp -- publish

If using yarn, there is no need to add the script entry. Yarn automatically searches for binaries in node_modules/.bin and uses them if present. It also does not need -- for argument forwarding:

$ yarn exp build:ios$ yarn exp build:status$ yarn exp publish

It may still be useful to add script entries as shortcuts to common commands:

# in package.json:"scripts": {"publish: "exp publish"...}

# usage$ npm run publish$ yarn publish

As pointed out by Kevin Kipp, another option is now available, npx, which is installed alongside npm as of npm 5.2.0. Invoking commands with $ npx instead of $ npm will automatically check for and, if present, run the local dependency:

$ npm install --save-dev exp$ npx exp publish

Just like yarn, local dependencies are preferred over a global versions of the same modules. Npx, however, goes a step further by automatically installing packages invoked which are not installed either locally or globally. In my opinion this feature will inevitably cause some confusion, surprises, and errors and as such is regrettable.

I’m glad to see this practice of installing everything as a local dependency is finally being embraced and supported by the community. Given the advantages I look forward to this being standard practice. It will take time for it to catch on so if you are a project maintainer with a project which instructs users to install globally, please consider changing the instructions to use a local dependency invoked via npx or yarn.


Published by HackerNoon on 2018/01/21