Alternatives to installing npm packages globally

[2022-06-18] dev, javascript, nodejs
(Ad, please don’t block)

There are two ways in which npm packages can be installed:

  • Locally, into a node_modules directory that npm searches for (or creates) in the current directory and its ancestors:

    npm install some-package
    
  • Globally, into a global node_modules directory:

    npm install --global some-package
    

    (Instead of the long version --global of this flag, we can also use the shorter -g.)

The latter requires root access on macOS and some other Unix platforms – which is a considerable downside. That’s why this blog post explores alternatives to global installs.

Preparation: changing the command line PATH  

In the remainder of this blog post, we need to change the command line PATH for some approaches. This PATH is a command line variable that lists all paths where the command line looks for executables when we enter a command. If we want to install executables via npm, it’s important that the PATH is set up correctly.

There are many good tutorials online, just do a web search for:

  • Windows: set path powershell
  • MacOS: set path zsh
  • Linux (e.g.): set path bash

On Windows, we can display the current PATH like this:

$env:PATH

On Unix, we can display it like this:

echo $PATH

Approach 1: changing the “npm prefix”  

The npm documentation recommends to change the npm prefix.

We can display the current prefix as follows (I’m showing the results for my Mac):

% npm config get prefix
/usr/local

Under that prefix, there are two important subdirectories.

First, a node_modules directory:

% npm root --global
/usr/local/lib/node_modules

Second, a bin directory which contains executable files:

% npm bin --global
/usr/local/bin

This directory is part of the macOS PATH by default. npm adds links from it into the global node_modules – e.g.:

/usr/local/bin/tsc -> ../lib/node_modules/typescript/bin/tsc

How do we change npm’s prefix?

Setup  

We create a directory and set npm’s prefix to that directory:

mkdir ~/.npm-global
npm config set prefix '~/.npm-global'

A tilde (~) on its own refers to the home directory on Unix and Windows. Instead of that symbol, we can also use the shell variable $HOME (on Unix and Windows), but must take care that shell variables are expanded.

Afterwards, we must add ~/.npm-global to the PATH.

Installing a package  

We can now continue to install packages with the flag --global, but they won’t be installed globally, they will be installed into our home directory:

npm install --global some-package

Pros and cons  

  • Pro: npm install --global works everywhere.
  • Con: No package.json of what’s installed makes reinstalls more work.
  • Con: npm itself is now also installed into ~/.npm-global (e.g. if you tell it to update itself).

Approach 2: installing into the home directory  

Another alternative to global installs is to install locally into a node_modules in our home directory and only set up the PATH correctly.

Setup  

We first turn our home directory into a package:

cd ~
npm init --yes

Then we add "~/node_modules/.bin" to our PATH.

Once we install our first package, the following new files will exist:

~/node_modules
~/package-lock.json
~/package.json

Installing a package  

Instead, of installing a package globally, we do this:

cd ~
npm install some-package

This adds at least the following directory to node_modules (possibly more, depending on how many dependencies some-package has):

~/node_modules/some-package

Per executable cmd that some-package provides, we also get:

~/node_modules/.bin/cmd -> ../some-package/bin/cmd

That is, the executable is a link into the package.

Pros and cons  

  • Pro: ~/package.json records all installed packages. That helps with reinstallations.
  • Con: We must go to the home directory before we can install a package.
  • Con: Three new files in the home directory – package.json, package-lock.json, node_modules.

Acknowledgement: This approach was suggested by Boopathi Rajaa.

Approach 3: installing into a subdirectory of the home directory  

This approach is a variation of approach 2. However, instead of turning our home directory into a package, we use a subdirectory of our home directory.

Setup  

mkdir ~/npm
cd ~/npm
npm init --yes

Then we add ~/npm/node_modules/bin to our PATH.

Once we install our first package, the following new files will exist:

~/npm/node_modules
~/npm/package-lock.json
~/npm/package.json

Installing a package  

cd ~/npm
npm install some-package

Pros and cons  

  • Pro: ~/npm/package.json records all installed packages. That helps with reinstallations.
  • Con: We must go to ~/npm before we can install a package.

Approach 4: using npx  

npx is an option if an executable that we are interested in has the same name as its package. (This is not a strict requirement but we have to type much more otherwise.)

It works as follows. If we install the executable cowsay globally and run it this way:

cowsay 'Moo'

Then we can also run it this way – without installing anything:

npx cowsay 'Moo'

The first time we use this command, npx downloads cowsay into a user-local cache and runs it from there. The download may take some time, but is only needed once. Thus, starting with the second time, running cowsay via npx is virtually as quick as running an installed version.

The npm documentation has more information on npx.

Pros and cons  

  • Pro: No installation necessary – which is great for executables we don’t need often.
  • Con: Running an executable means more typing.
  • Con: Isn’t really an option if an executable doesn’t have the same name as its package.
  • Con: Makes it more difficult to prepare for being offline.

Approach 5: using a Node.js version manager  

There are tools that let us install multiple Node.js versions and switch between them – for example:

These tools usually set the npm prefix to a directory somewhere inside the current home directory.

Acknowledgement: A discussion on Twitter helped me with writing this blog post. Thanks to everyone who participated!