npm is at the core of developing in Node. Most of us use it to get packages, set up the package.json etc.
But when you put it together with the vast possibilities in package.json you get another gear. Then npm can actually become a build tool - driving every aspect of your development. Without getting unwieldy and out of control.
In this post I wanted to examine one of the lesser known features of scripting with npm and package.json; pre
- and post
hooks. In doing so I will most likely brush on a lot of other subjects too. We’ll see how it goes.
[UPDATED] This, and other posts on npm scripting, has drawn a lot of attention. I wanted you to know that I have created a PluralSight course on this topic, published in late October 2015. You can find it here.
Also, don’t miss the other posts on this blog on npm scripting:
- npm scripting: git, version and deploy
- npm scripting: configs and arguments… and some more tricks
- Pre and Post hooks for npm scripting
If you liked this post I know you will love the course! Thank you for reading this.
I’ll keep my code here
The scripts
Pretty soon after starting to use the package.json file we start to use the scripts
-node. In fact when you use npm init
to set up your package.json
file a test script is created for you. Often it looks something like this:
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
}
That’s not much to look at (and you should probably change it to something that actually does something useful, like running your tests), but it can still teach us some about the scripts
-node.
So, a command is defined for me. In this case an echo
reminding me to add some test facilities to my application / package.
How can I run this?
All scripts under scripts
can be executed with npm run-script [script name]
. Like this, to run our test command:
npm run-script test
This command with simply just run whatever code found under the test
-key of the scripts
-node.
Luckily there’s a short form too and it’s more commonly used:
npm run test
This is awesome because that means that we now can have scripts in our package.json
that we can use npm
to execute. A first, vital, step towards a build tool.
test and start
There are two scripts that npm
supports out of the box; test
and start
. When I write supports it’s more like; there’s very convenient, well-known short cuts for them.
So instead of npm run start
we can go npm start
. Testing is even cooler; we can choose from these options:
npm run test
npm test
npm tst
npm t
But the really useful thing about these “well known” scripts is that tools and infrastructure in the Node world knows that they exists and can make use of them. For example, when you push code to Heroku it will launch your application with npm start
. Or you continuous integration platform might run test, by convention by simply executing npm test
.
Hooks; pre and post
Before I get completely side tracked here let’s go back to the theme of the this post; pre and post hooks.
This is one of the lesser known and used features of npm scripting I think. And it’s a bit funny that this is “lesser” known and used, since it’s actually the first thing that they write about in the documentation.
Ok anyway; all scripts supports pre
and post
hooks or scripts. They are automatically run by npm
before (pre
) and after (post
) their respectively script / command. Aka JustWhatIThought (tm)
The pre/post scripts are for all intents and purposes just another script in your package.json
file. They are also defined under the scripts
-node.
For example; the script you’ve defined (if any) under pretest
is run before the test
script. And subsequently the posttest
script is run after.
Here’s an example;, you can find all my code here:
"scripts": {
"test": "mocha -u bdd -R spec",
"pretest": "rm -r ./logs/ && mkdir logs",
"posttest": "rm -r ./logs/ && mkdir logs"
}
In this contrived example my tests creates some log files and I need to remove them, and recreate the folder, before (and after) I run my test. Easy to accomplish with the pre
and post
hooks. If you haven’t seen it &&
is a simple way of running a few commands in a row.
Note that you should probably clean up test-data in your test code but it works as an example
Hooks out the box
There’s quite a few hooks that npm
knows about out of the box, and hence will run for you, if they are defined:
- The
npm
commandpublish
will run:prepublish
- then
publish
- and finally
postpublish
install
haspreinstall
andpostinstall
uninstall
also have pre and post hooksversion
test
, that we talked aboutstart
stop
restart
For all of these there’s respectively pre
and post
hook. This comes very handy to manage the life cycle of your application.
Custom scripts
What makes npm
more useful, and what actually makes it a viable build tool, is that you can define your own, custom scripts.
Remember that you npm test
actually was a short-cut for npm run test
(which in turn is a short-cut for npm run-script test
)?
Well there’s nothing stopping you from defining your own script and run them. Here’s a script that I’ve defined to run jslint over my code. The “tool that makes developer cry”. I’m one of those guys I guess…
"scripts": {
"lint": "jslint '*.js'"
}
I can now run this by simply going npm run lint
and it will lint all my files in the root directory.
Yes, I’ve could have defined the script as:
"scripts": {
"lint": "jslint '**/*.js'"
}
to run it on all my files, but I grew tired trying to fix the lint issues with mocha. Sorry.
Dependencies and devDependencies
Now, there’s a dependency problem here - that jslint
command? What is that? Psst… a Node package.
And now everyone has to have it installed globally? No. This is a development time dependency for my package. It only needs to be installed if you’re going to run npm run jslint
.
This is what devDependencies
in the package.json
is for. You can install and save the dependency like this npm install jslint --save-dev
.
We don’t have to give the path to the node_modules
-folder (./node_modules/jslint/bin/jslint
) since npm will look for packages first the local node_modules
and then continue upwards.
Pre and post hooks for custom script
Now, while that npm run lint
is cool, I still have to remember to run it from time to time. If only I could run it just before testing, failing if the linting didn’t pass 100%. What to do? What to do?
Aha! pretest
! Yes, folks - this is a perfect scenario to run my custom lint
script. Let’s add it to the pretest
script:
"scripts": {
"lint": "jslint '*.js'",
"test": "mocha -u bdd -R spec",
"pretest": "npm run lint && rm -r ./logs/ && mkdir logs",
"posttest": "rm -r ./logs/ && mkdir logs"
}
Now then I, or my continuous integration server or what have you, go npm test
to test my application the following happens:
- The
pretest
script is executed- it’s first command
npm run lint
is run, and halt execution ifjslint
returns errors - we then clean the
./logs/
folder - and recreate it for good measures
- it’s first command
- Now the
pretest
script is fully executed and thetest
script is run
Refactor the script
My package.json
is doing what I want, but starting to look messy. For example the pretest
script is a bit ugly to read now, don’t you think? Also the exact same script is used for posttest
Let’s fix it by refactoring a new script to clean logs. Like this:
"scripts": {
"cleanlogs" : "rm -r ./logs/ && mkdir logs",
"lint": "jslint '*.js'",
"test": "mocha -u bdd -R spec",
"pretest": "npm run lint && npm run cleanlogs",
"posttest": "npm run cleanlogs"
}
There - that’s better. Now I simply call into my custom cleanlogs
script from pretest
and posttest
. The pretest
script calls two scripts after each other.
I played around with actually installing the jslint
as a prelint
task. It works but I don’t think this is a good idea:
- The dependencies that is need to develop should be listed under
devDependencies
. This adds clarity and understanding. - Updating the
package.json
feels … dirty me thinks
That said - it’s totally possible. Here’s how:
"scripts": {
"lint": "jslint '*.js'",
"prelint": "npm install jslint --save-dev"
}
But I’d advice against it.
The ‘npm run’ command
Let me leave you with a nifty little feature more of npm
. If you only go npm run
, without any parameters, npm
will list all scripts in your package.json
. Like a poor-mans-help. Here’s what comes out if you go npm run
on the file we’ve created:
Lifecycle scripts included in preposthooks:
test
mocha -u bdd -R spec
pretest
npm run lint && npm run cleanlogs
posttest
npm run cleanlogs
available via `npm run-script`:
cleanlogs
rm -r ./logs/ && mkdir logs
lint
jslint '*.js'
Summary
As you can see the pre
and post
hooks are very powerful allies if you want to use npm
and package.json
as a build tool.
You can find my code here
I’ll write a post more about how we can integrate git, pushing and deploying into such script next.
I hope you liked this post.