How I can get away with never installing npm packages globally

RMAG news

My belief is that when you clone a git repository all code, settings and tools should be contained in that cloned repo; the directory. It should not contaminate the system, not by tools, not by environment variables, maybe a bit for the platform.

Scope

This article will not discuss the use of .env file for maintaining environment variables or the .nvmrc to enable automatically switching to the proper node version. Also it will not include info about .npmrc file to e.g. lock the node version.

Trigger to write this article

So many README.md files or manuals say things like:

// https://angular.io/guide/setup-local

npm install -g @angular/cli

More candidates like these are, but not limited to:

eslint
prettier
create-react-app
webpack
@vue/cli
nx

Why would you want to do this? You have now local packages to maintain, and you can’t use it across different projects that may have different versions of that software package.

Enter npx

npx is a package runner tool that comes with npm 5.2.0 and higher. The current version when writing this article is npm 10.8.0 It is designed to execute binaries from Node packages without globally installing them. However, NPM documentation suggests globally installing certain packages, especially when they are CLI tools that will be used frequently across different projects.

So instead of installing globally you can execute:

npx @angular/cli

Or even if you want to use a specific version of @angular/cli

npx @angular/cli@17

You have to type less, you can specify exact versions and you don’t contaminate the developer’s machine.

But what about package.json?

The package.json file serves as the cornerstone of any Node.js project, acting as the project’s manifest. It provides critical metadata such as the project’s name, version, and author, and it specifies the dependencies and devDependencies required for the project to run and be developed. Additionally, package.json defines custom scripts that automate common tasks like building, testing, and starting the application. This file ensures consistent environment setup, simplifies dependency management, and facilitates project configuration, making it an essential tool for maintaining and sharing Node.js projects.

So if it is a dependency of the project, why not add it to the dependencies, devDependencies or even peerDependencies?

You can specify the exact versions, using carot-minor (^) or tilde-patch (~) imports. It even automatically updates when fixes arrives.

But I can’t run the tools’ commands directly from the shell?

Well, fair point. If you want to run e.g. eslint, you don’t want to enter:

node_modules/eslint/bin/eslint.js .

enter npm scripts.

You can simply write:

{
“scripts”: {
“lint”: “eslint .”
}
}

So why does this work?

NPM scripts can locate executables in the project’s node_modules/.bin directory without needing to specify the full path. I does also look at the project itself and the global installed packages and eventually just performs the command hoping the OS will pick it up.

So that does mean that you can also install e.g. nx and use a script to alias the nx command:

{
“scripts”: {
“nx”: “nx”
}
}

But what about passing arguments?

You can’t simply pass arguments to nx as nx –help, you need to pass them as positional arguments using an extra pair of dashes — like so as described in the npm documentation:

nx –help

So that is why I don’t need to install any global dependency. Ever. Because we have npx and npm scripts.