TIL: You don't need npx in your package.json scripts

Mar 21, 2021

When using a tool like mocha or eslint, it's a pretty common pattern to install the package globally and then run it:

npm install -g eslint
eslint src/**/*.ts

Global packages work fine when you only need to run the tool on your machine, but what happens if you need to run eslint on CI or if your teammates need to run it?

Surely everyone shouldn't have to manually install a global package before using your project, right? And they'll probably end up using installing a different version to you.

There are a couple better options available.

Use npx

You can use npx to run the command locally: npx eslint src/**/*.ts or add the command to your package.json scripts:

{
  "scripts": {
    "lint": "npx eslint src/**/*.ts"
  }
}

Now anyone on your team can simply run npm run-script lint and it'll work! Nice.

But how do we ensure that everybody's using the same version of eslint (especially if this task is being run on CI)?

Install the package locally

We can install eslint as a local dev package and run it from there:

{
  "devDependencies": {
    "eslint": "^7.2.0"
  },
  "scripts": {
    "lint": "./node_modules/.bin/eslint src/**/*.ts"
  }
}

That's a bit better. Now anybody using our project can run npm install and then npm run-script lint to do linting, and we're certain that everybody will be using the same version.

But it's not perfect. It's not very readable (especially if you have a bunch of scripts). We can do better.

We don't actually need ./node_modules/.bin

It turns out that npm run-script (and therefore npm run) adds ./node_modules/.bin to your shell's PATH, so we don't actually need the node_modules path.

Now this is what our package.json looks like in its final form:

{
  "devDependencies": {
    "eslint": "^7.2.0"
  },
  "scripts": {
    "lint": "eslint src/**/*.ts"
  }
}

Doesn't that look better?