How to use npm as a build tool
No-nonsense guide on how to replace standard pre-processor tasks using just NPM. No Grunt. No Gulp. Just package.json. Yes - it's possible! You've come here because you've heard the buzz about replacing pre-processors like Grunt or Gulp with straight up NPM and you want to find out more.
In this guide we'll learn how to replicate some standard pre-processing tasks like JS linting, minifying, ES2015 to ES5, compiling CSS, and even the ever useful watch task. We'll start with a brief intro on how exactly npm can work as a build tool and then we'll look at each task individually.
You can read through start to end or, once you're familiar with the basics, you can skip to a task that interests you.
How to use npm as a build tool?
We need two things:
- A terminal/command prompt with npm installed
- A
package.json
file
The key to this lies inside the package.json file. Check out this basic example below and you'll see a scripts
object along with the more familiar parts. The properties inside this object are tied to the command npm run
.
{
"name": "project",
"version": "1.0.0",
"description": "Using npm as a build tool",
"author": "Adrian Payne",
"scripts": {
"help": "echo We'll figure it out. We'll use the Force."
}
}
Let's take a closer look inside our scripts
object. This isn't just an ordinary object. It can be harnessed and used to run commands.
"help": "echo We'll figure it out. We'll use the Force."
The key, help
, is the command name and the value echo We'll figure it out. We'll use the Force.
is the command that npm will attempt to run. Let's run our help
command and see what happens.
Yes, it's Windows ;) but this guide will work for Linux systems. Mac should work too, though I don't have one around to test.
The -s
is a configuration parameter internal to npm. It stops npm from logging extra info that we don't need to see for our use case. Feel free to omit it and see what happens.
Now you know the basics! In the sections below, we'll be using the same format to build out commands that run tasks like linting and compiling SCSS. We'll install the same or similar packages that we would have used in Grunt or Gulp and run commands that they would have been running behind the scenes anyway but all we need is the package.json file! :)
Command Hooks
We can take this one step further with the useful pre
and post
hooks for a command. These will run before and after our command. They are individually optional. We set them up by prefixing our command name with either "pre" or "post. They are then automatically called by npm when we run npm run help
. In the example below, we're just printing some text on screen but you can take advantage of this feature for doing things like linting or running tests before compressing your code.
"prehelp": "echo Welcome to help."
"help": "echo We'll figure it out. We'll use the Force."
"posthelp": "echo There is no more help :(."
How do I compile SCSS into CSS with npm?
We'll use node-sass for this task. Documentation.
1) Add the package to your project and package.json with npm install node-sass --save-dev
2) Add the command to your scripts
block. Here we have a source file followed by the output file. For more command options, check out the package documentation.
"scss": "node-sass scss/source.scss public/css/styles.css"
3) Run the command with npm run scss
Bonus
node-sass also offers the option to compress and include a sourcemap file. We can take advantage of those features by adding some extra parameters --output-style compressed
and --sourceMap true
. The parameter --quiet
just silences some extra information the compiler shows.
"scss": "node-sass --quiet --output-style compressed --sourceMap true resources/scss/source.scss public/css/styles.min.css"
How do I minify JS with npm?
We'll use uglify-js for this task. Documentation.
1) Add the package to your project and package.json with npm install uglify-js --save-dev
2) Add the command to your scripts
block. Here we have a source file followed by the output file. For more command options, check out the package documentation.
"uglifyjs": "uglifyjs js/source.js -o public/js/scripts.min.js"
3) Run the command with npm run uglifyjs
How do I lint JS with npm?
We'll use jshint for this task. Documentation.
1) Add the package to your project and package.json with npm install jshint --save-dev
2) Add the command to your scripts
block. Here we have a source file followed by the output file. For more command options, check out the package documentation.
"lint": "jshint js/ -s"
3) Run the command with npm run lint
Bonus
If you're coding with es2015/ES6, you'll need to complete one more step so that jslint understands the new syntax - simply add the following to your package.json
configuration.
"jshintConfig": {
"esversion": 6
}
How do I convert ES6 (es2015) into ES5 with npm?
For this we're going to use Babel. Documentation.
1) Add the package to your project and package.json with npm install babel-cli --save-dev
2) Add preset for converting es2015 with npm install babel-preset-es2015 --save-dev
3) Add the command to your scripts block. Here we are telling Babel to compile all files in the js/ folder into one file named compiled.js. We also tell it to use our installed preset for es2015.
"babel": "babel js/ --out-file public/js/compiled.js --presets es2015"
4) Run the command with npm run babel -s
How do I run multiple tasks at once with npm?
To run tasks at the same time, we can separate run commands with &&
"compress": "npm run minify:css && npm run minify:js"
To run tasks one after another, we can separate run commands with a single &
"build": "npm run lint -s & npm run compress"
How do I run a watch task with npm?
Yes, it's possible to do this with only npm. We'll use watch for this task. Documentation.
1) Add the package to your project and package.json with the npm install watch --save-dev
2) Add the command to your scripts
block. Here we have told the watch task to watch a js folder for changes and, when found, run a task called build. It will also ignore files like .gitignore with the --ignoreDotFiles
parameter.
"watcher": "watch \"npm run build -s --ignoreDotFiles\" js/"
3) Run the command with npm run watcher
Putting it all together
In a future post I'll give an example of a more complete build tool package.json config. But basically you just build on all of the concepts above, taking advantage of pre and post hooks along the way. Here's a quick preview of a more complete setup in action
Concerns
While I'm going to take the plunge and stick to this "new" method from now on, I do have a couple of niggling concerns...
The first is that you can't add comments in a JSON file, so the documentation for the commands in your package.json will have to live, exclusively, elsewhere. To be honest, I think that's fine. These days, teams probably (read: should) have a dedicated place for project documentation and can easily explain the package.json file there.
The second concern involves the potential increasing complexity of a build process. The scripts section could get quite big and when you have a lot of pre's, post's, and commands invoking other commands, it might get a bit tricky to track... but it's not insurmountable. As developers, we're generally familiar with running commands and I don't think it's such a big deal to take a few seconds to track down a path in a one-liner and you can always extract it out into a config
block. See my post on Using config variables in package.json.
Final Thoughts
I'll be honest. When I first heard of this method, I put on my hat of negativity and my colleague bore the brunt of that rant in Slack. Sorry, Austin! I've been a promoter of pre-processors like Grunt and Gulp, introducing Grunt to a number of teams and projects while at Goodgame Studios and using it in almost every personal project since. I couldn't believe you could completely remove it and be fine... but it turned out that you could and it removed a whole wrapper of abstraction to boot.
After just an hour of tinkering around with this npm only method, I wished I'd heard of it first!