Demystifying NPM Scripts
Tags: npm, node, bash, beginners
Published: 2021-01-17 13:18:05

If you're new to Node or have only worked on projects that already have their npm scripts set up, then you may be wondering what npm `scripts` are and how they work. In this article, I'll give my best intuitive explanation for how/why they work and highlight some of the key tools available for writing simple npm scripts.

## What Are These Scripts?

### `package.json`

[`package.json`](https://docs.npmjs.com/cli/v6/configuring-npm/package-json) is the npm configuration file for a project, including its dependencies, project details, and `scripts`. [`npm run`](https://docs.npmjs.com/cli/v6/commands/npm-run-script) is the npm command that executes commands written in the `scripts` section.

For my [template article repo](https://github.com/dcwither/articles-template/blob/main/package.json), I set up a linter script `lint`, which is run with `npm run lint`. Running `prettier **/*.md` in the terminal directly wouldn't work because `prettier` isn't found in the global `PATH`.

```json
"scripts": {
  "lint": "prettier **/*.md"
},
"devDependencies": {
  "prettier": "^2.1.2"
}
```

### Meet `PATH`

`PATH` is an environment variable that lists directories where the shell should look for commands. You can find a more thorough explanation on [Linux Hint](https://linuxhint.com/path_in_bash/).

In order to find out what's inside your `PATH` you can run `echo $PATH`. Running that on a [`repl.it` sandbox](https://repl.it/@dcwither/npm-scripting-tutorial), I got the following:

```bash
~/npm-scripting-tutorial$ echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
```

At least for our purposes, all `npm run` does is append a few more directories onto the `PATH`. We can confirm this by making the following npm script, and running it ourselves:

```json
"scripts": {
  "path": "echo $PATH",
  "lint": "prettier **/*.md"
}
```

```bash
~/npm-scripting-tutorial$ npm run path

> @ path /home/runner/npm-scripting-tutorial
> echo $PATH

/usr/local/lib/node_modules/npm/node_modules/npm-lifecycle/node-gyp-bin:/home/runner/npm-scripting-tutorial/node_modules/.bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
```

So `npm run` prepended the following section to the `PATH`:

```
/usr/local/lib/node_modules/npm/node_modules/npm-lifecycle/node-gyp-bin:/home/runner/npm-scripting-tutorial/node_modules/.bin
```

The `/home/runner/npm-scripting-tutorial/node_modules/.bin` section is what gives us access to `prettier` in the `npm lint` command.

### What Does This Mean?

The expansion of the `PATH` is what allows us to call commands written in other npm packages without referencing their exact location in our `node_modules` folder. If we didn't have this, our scripts would look more like the following:

```json
"scripts": {
  "lint": "./node_modules/.bin/prettier **/*.md"
}
```

That wouldn't be terrible, but it's not exactly ideal.

## Other Highlights

### Aliases

Some commands are so common that npm aliases them so they don't need to be prefixed by `run`. These include:

- [`npm build`](https://docs.npmjs.com/cli/v6/commands/npm-build)
- [`npm start`](https://docs.npmjs.com/cli/v6/commands/npm-start)
- [`npm stop`](https://docs.npmjs.com/cli/v6/commands/npm-stop)
- [`npm test`](https://docs.npmjs.com/cli/v6/commands/npm-test)

So running `npm start` is the same as running `npm run start`.

### Lifecycle Operations

There are some names that are hooks into steps of npm lifecycle commands (e.g. `npm publish`, `npm install`, `npm start`). You can add scripts with these names to trigger commands at those steps:

```json
"scripts": {
  "build": "tsc --project .",
  "prepack": "npm run build"
}
```

One unintuitive quirk with lifecycle scripts is that [`prepare` and `prepublish`](https://docs.npmjs.com/cli/v6/using-npm/scripts#prepare-and-prepublish) (both of which are now deprecated) also trigger on local `npm install`, so if you have a build step that shouldn't trigger on install, it would be better associated with `prepublishOnly` or `prepack`.

The docs include more info on other [lifecycle operations](https://docs.npmjs.com/cli/v6/using-npm/scripts#life-cycle-operation-order) that you can hook into.

### Command Arguments

Normally if we pass in a `--option` to `npm run`, it won't pass it through to the command written in `scripts`. For instance, to make `prettier` automatically fix issues, we would want to pass the `--write` option. If we add a `--` before the options, they will be passed through. This means we update our `npm run lint` command above to the following to execute `prettier --write`:

```bash
npm run lint -- --write
```

## Conclusion

Hopefully this quick intro to the concepts around npm scripts makes it easier to read the scripts you encounter, as well as start writing your own. If you have any other questions, I recommend you look through npm's well-written documentation, starting with the [npm CLI docs](https://docs.npmjs.com/cli/v6).