npm run-scripts explained
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:
- Create a folder called `sports` and remove it first, if it exists.
- Ask the user to type in their favourite sport.
- 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.