npm run-scripts explained

npm has a rich ecosystem, which makes it possible to run custom scripts from the terminal. These scripts are essential parts of the developer's life and are used every day for testing, linting or compiling. Let's see the most important features of run-scripts.

npm is the most widely used package manager among developers. I wrote a post about the basics a few weeks ago, and it might be worth reading that post first, if the reader is not familiar with some of the concepts here.

We can start a new project from scratch with npm init, and as a result the package.json file is created with a scripts object in it. Commands related to installed packages or other scripts shall be placed in this scripts object.

Simple commands

Scripts defined in the scripts object are called run-scripts because they can be run with the npm run SCRIPT_NAME command. No surprise here.

In some cases, we can leave off the run command though.

test

First, when the package.json file is created, the scripts object already has a property called test with a build-in command value:

// ...
"scripts": {
  "test": "echo \"Error: no test specified\" && exit 1"
},
// ...

This is how npm tries to convince developers to write tests. :)

The value of the test property can be run without specifying run, and it’s enough to type npm test. In the above case we will see Error: no test specified in the terminal as well as some error messages from Node.js as the process exits.

The value of the test property will hopefully be replaced with the commands of a testing framework at the beginning of the development process. You can read more on writing tests with Mocha and Chai here, here and here.

start and other commands

test is not the only command that runs without run. If a Node server is set up in the project folder or a bundler like webpack or Parcel is used, the start command is very often used for starting the (development) server.

start can also be run without run just by typing npm start and it defaults to the node server.js command. This means that even if the start property doesn’t exist in scripts in package.json but a server.js file in the root folder does, the content of server.js can still be run with npm start.

Of course, one can specify their own start command if the file is called somehow else or is located in another folder or a development server of a bundler is used.

For example, the following command will start Parcel’s dev server:

// ...
"scripts": {
  "start": "parcel index.html"
},
// ...

Similarly, the stop, restart and install commands also run without adding run. You can read more on these commands on the npm website.

No need to point at node_modules

Assume we want to run our tests with Mocha. In this case we can write something like this in the test property:

"test": "mocha FOLDER/**/*.test.js"

References to the locally installed packages (and their dependencies) that can be run are placed in the node_modules/.bin folder. npm places this folder to PATH, so we don’t have to manually add node_modules/.bin to the script if we want to run a package from the project folder.

Custom commands with run

But we can write our own scripts as well!

These scripts can be related to packages that are installed in the project folder (e.g. linters or compilers) or they can be other scripts, like ones written in bash or JavaScript.

Let’s have a look at an example. We will write a similar script in both languages: They will ask the user to choose their favourite sport from those displayed in the terminal.

Bash scripts

Assume we have the following bash script in a file called sport.sh and say that the file is located in the root project folder:

#!/usr/bin/env bash

Sport=('Soccer' 'Cricket' 'Badminton' 'Basketball' 'Ice Hockey')

echo "Which sport do you like the most? Enter the number."
for i in {1..5}; do
  echo "$i: ${Sport[$i - 1]}"
done

read response
echo "You like ${Sport[$response - 1]} the most."

This is a very simple script, which starts with declaring an array of five sports. Then we iterate over the elements, ask for the user’s response and finally write the answer to the terminal.

We can create an entry in the package.json file like this:

// ...
"scripts": {
  "sport": "sh sport.sh"
}
// ...

We can then type the npm run sport command in the terminal and the script will first display the choices, then asks for our response and finally shows our choice. (I hope that I managed to put a sport you like on the list.)

JavaScript scripts

Not only bash scripts can be run through npm. Let’s now have a slightly more complex series of commands in JavaScript.

The flow is the following:

  1. Create a folder called `sports` and remove it first, if it exists.
  2. Ask the user to type in their favourite sport.
  3. Save the answer in a text file inside the `sports` folder.

This little exercise seems to be dumb but it points at some interesting features of run-scripts.

Points 2 and 3 can be managed in a JavaScript file called sport.js. Let’s create it in the root folder:

const fs = require('fs');

console.log('Type your favourite sport and press Enter:');

const input = process.stdin;
input.on('data', function(sport) {
  fs.writeFileSync('./sports/my-sport', sport);
  console.log(`Your favourite sport is ${ sport } and it has been saved to your sports folder!`);
  process.exit(0);
});

Here we create a readable stream which takes the user input form the console (stdin). When a chunk of data is available (i.e. we typed something and pressed Enter), the favourite sport will be written in a file called my-sport and then we will give a feedback to the user in the console. Finally, we exit the process.

Let’s create then the script command in the package.json file:

"scripts": {
  "sport:stream": "rm -rf sports && mkdir sports && node sport.js"
}

We first remove the sports folder and its content if exists (rm -rf sports). We then create the sports folder (mkdir sports), and finally run the content of sport.js (node sport.js).

We run three commands here by concatenating them with &&, so we can run them in one npm command (sport:stream).

Let’s type npm run sport:stream in the terminal and the script in sport.js will run. We can read our answer in the file created in the sports folder.

Run other npm commands inside sport:stream

The sport:stream command has become quite verbose. We can separate the phases into their own npm commands to make the scripts more readable:

// ...
"scripts": {
  "remove": "rm -rf sports",
  "create": "mkdir sports",
  "sport:stream": "npm run remove && npm run create && node sport.js"
},
// ...

Now the npm run remove command will remove the sports folder and the npm run create command will create it again inside the project root.

As it can be seen these npm commands can be run from inside sport:stream! By entering npm run sport:stream to the terminal we’ll get the same result as last time.

Pre and post hooks

But we can do even better with the pre and post hooks.

These hooks as their name suggest will run a script before and after the related script, respectively. All we have to do is to prepend the main script with pre and post.

Let’s refactor the scripts object in package.json:

// ...
"scripts": {
  "precreate": "rm -rf sports",
  "create": "mkdir sports",
  "presport:stream": "npm run create",
  "sport:stream": "node sport.js"
},
// ...

We can rename the remove command to precreate, which will result the rm -rf sports command in running right before the mkdir sports. This is exactly what we want: Remove the sports folder first if it exists.

Then we can create a presport:stream command, where we run the create script with npm run create. So the precreate and create scripts will run inside the presport:stream command.

All we have to do now is to type npm run sport:stream again and the result will be the same but this time the scripts object is cleaner and more readable.

Going crazy

But that’s nothing, we can do even better!

Using the post hook, we can decrease the number of necessary commands while getting the same end result:

// ...
"scripts": {
  "presport:stream": "rm -rf sports",
  "sport:stream": "mkdir sports",
  "postsport:stream": "node sport.js"
},
// ...

Although this is all nice and works well, it comes with a sacrifice. The main script which does the biggest part of work, so to speak, is inside postsport:stream and sport:stream doesn’t exactly do what it says: It only creates a folder.

I think (and this is only my personal preference) that node sport.js should definitely be in a separate command with its own descriptive name, like in the previous example, even if we have one more command in scripts. It works either way, there’s no one truth here.

The silent flag

When the npm run sport:stream command is run, npm displays a bunch of information, like the name of the package, the version number (1.0.0 by default), the run-script command(s) that are invoked and the path to folder.

This might be unnecessary in some cases and we only want the result of our run-script without having to read all the logs about what we know anyway.

If the --silent flag is applied, npm won’t write these pieces of information to the console. All we have to do is to append the flag after the run command: npm run sport:stream --silent.

Arguments

npm run-scripts can accept arguments as well. They shall be separated by -- and the arguments can come afterwards.

Insert the following to sport.js just before the const input = process.stdin; line:

process.argv[2] === 'ball'
  ? console.log('Type your favourite ball sport and press Enter:')
  : console.log('Hmmm. Type your favourite sport anyway and press Enter.');

This piece of code tells that if we have an argument of ball, we will display the first prompt to the user and if it’s something else (or doesn’t exist), we will show the second message. Balls sports are the best, after all.

process.argv returns the command line arguments in an array. The first two elements are reserved for the path to Node.js and to the script to be executed, respectively. This is why we have to refer to the third element if we want to access our custom argument.

Now type npm run sport:stream --silent -- ball and the first message will appear (note the space before and after the --). If the ball argument is left off or you write something else instead, the message starting with Hmmm. will be displayed.

Conclusion

This is it about npm run scripts for now. These are the core features of run scripts and should be enough for the reader to get started.

It’s worth noting that scripts can be run with npx as well. This post is already long enough, so I won’t mention it here, but if anyone interested, they can more about the how’s here.

Thanks for reading and see you next time.