Give Grunt the Boot! A Guide to Using npm as a Build Tool

Peter Dierx
Peter Dierx
Share

Front-end build and workflow tools are available in abundance: Grunt, Gulp, Broccoli, and Jake to name but a few. These tools can automate almost anything you find yourself doing repeatedly in a project, from minifying and concatenating source files, to running tests or compiling code. But the question is, do you need them? Do you really want to introduce an additional dependency to your project? The answer is “No!”. There is a free alternative that can do the majority of these tasks for you and it comes bundled with Node.js. Of course I’m talking about npm.

In this article we’ll discuss what npm is capable of as a build tool. If you’d like a quick primer on npm before starting, please refer to our beginner’s guide to npm. If you’d like to follow along, you can find the code used in this article on GitHub.

npm Scripts

To start our discussion, we’re going to create a directory for our new demo project, that we’ll call “buildtool”. Once done, we’ll move into this folder and then run the command npm init to create a package.json file:

$ mkdir ~/buildtool && cd ~/buildtool
$ npm init

You’ll be asked several questions. Feel free to skip all or part of them as you’ll replace the final content of the package.json file with the following content:

{
  "name": "buildtool",
  "version": "1.0.0",
  "description": "npm as a build tool",
  "dependencies": {},
  "devDependencies": {},
  "scripts": {
    "info": "echo 'npm as a build tool'"
  },
  "author": "SitePoint",
  "license": "ISC"
}

As you can see, we have a scripts object with a property called info. The value of info is going to be executed in the shell as a command. We can see a list of the scripts properties (also known as commands) and values defined in a project by running the command:

$ npm run

If you run the previous command in our project folder, you should see the following result:

Scripts available in buildtool via `npm run-script`:
  info
    echo 'npm as a build tool'

In case you want to run a specific property, you can run the command:

$ npm run <property>

So, to run the info command we defined in the package.json file, we have to write:

$ npm run info

It’ll produce the following output:

$ npm run info
> buildtool@1.0.0 info /home/sitepoint/buildtool
> echo 'npm as a build tool'

npm as a build tool

If you only want the output of info, you can use the -s flag which silences output from npm:

$ npm run info -s
npm as a build tool

We only used a simple echo so far, but this is a very powerful feature. Everything on the command line is available to us and we can be very creative here. So let’s build on what we’ve covered up to this point and install some packages to create some common workflows.

Common Workflows

The first thing we would like to implement is a linting capability for our JavaScript files. This involves running a program that will analyse our code for potential errors. We are going to use JSHint for this, so the first step is to install the package via npm:

$ npm install jshint --save-dev

After you execute this command, you’ll see a new subfolder named node_modules. This is where JSHint has been downloaded. In addition, we also need to create the following folder structure for our project:

├── assets
│   ├── css
│   │   └── main.css
│   └── scripts
│       └── main.js
├── dist
├── package.json
├── node_modules
└── test
    └── test.js

On a Unix system, this can be done with the following command:

$ mkdir -p assets/css assets/scripts test && touch assets/css/main.css assets/scripts/main.js test/test.js

Linting

Now we’ll force some syntax errors in the main.js file. At the moment the file is empty, so open it and paste in the following content:

"use strict";

var Author = new function(name){
  this.name = name || "Anonymous";
  this.articles = new Array();
}

Author.prototype.writeArticle = function(title){
  this.articles.push(title);
};

Author.prototype.listArticles = function(){
  return this.name + " has written: " + this.articles.join(", ");
};

exports.Author = Author;

var peter = new Author("Peter");
peter.writeArticle("A Beginners Guide to npm");
peter.writeArticle("Using npm as a build tool");
peter.listArticles();

Hopefully the intent of this code is clear — we are declaring a constructor function whose purpose it is to create new Author objects. We also attach a couple of methods to Author’s prototype property which will allow us to store and list the articles an author has written. Notice the exports statement which will make our code available outside of the module in which it is defined. If you’re interested in finding out more about this, be sure to read: Understanding module.exports and exports in Node.js.

Next, we have to add a property to our scripts object in package.json that will trigger jshint. To do that, we’ll create a lint property as follows:

"scripts": {
  "info": "echo 'npm as a build tool'",
  "lint": "echo '=> linting' && jshint assets/scripts/*.js"
}

Here we’re taking advantage of the && operator to chain the commands and file globs (the asterisk) which gets treated as a wildcard, in this case matching any file with a .js ending within the script directory.

Note: the Windows command line does not support globs, but when given a command line argument such as *.js, Windows passes it verbatim to the calling application. This means that vendors can install compatibility libraries to give Windows glob like functionality. JSHint uses the minimatch library for this purpose.

Now let’s lint the code:

npm run lint -s

This produces the following output:

=> linting
assets/scripts/main.js: line 1, col 1, Use the function form of "use strict".
assets/scripts/main.js: line 5, col 28, The array literal notation [] is preferable.
assets/scripts/main.js: line 3, col 14, Weird construction. Is 'new' necessary?
assets/scripts/main.js: line 6, col 1, Missing '()' invoking a constructor.
assets/scripts/main.js: line 6, col 2, Missing semicolon.
assets/scripts/main.js: line 16, col 1, 'exports' is not defined.

6 errors

It works. Let’s clean up those errors, re-run the linter to make sure, then move on to some testing:

(function(){
  "use strict";

  var Author = function(name){
    this.name = name || "Anonymous";
    this.articles = [];
  };

  Author.prototype.writeArticle = function(title){
    this.articles.push(title);
  };

  Author.prototype.listArticles = function(){
    return this.name + " has written: " + this.articles.join(", ");
  };

  exports.Author = Author;

  var peter = new Author("Peter");
  peter.writeArticle("A Beginners Guide to npm");
  peter.writeArticle("Using npm as a build tool");
  peter.listArticles();
})();

Notice how we have wrapped everything in an immediately invoked function expression.

npm run lint -s
=> linting

No errors. We’re good!

Testing

First we need to install the mocha package. Mocha is a simple, yet flexible JavaScript test framework for Node.js and the browser. If you’d like to read more about it, this article is a great place to start: Basic Front End Testing With Mocha & Chai

npm install mocha --save-dev

Next we are going to create some simple tests to test the methods we wrote previously. Open up test.js and add the following content (notice the require statement which makes our code available to mocha):

var assert = require("assert");
var Author = require("../assets/scripts/main.js").Author;

describe("Author", function(){
  describe("constructor", function(){
    it("should have a default name", function(){
      var author = new Author();
      assert.equal("Anonymous", author.name);
    });
  });

  describe("#writeArticle", function(){
    it("should store articles", function(){
      var author = new Author();
      assert.equal(0, author.articles.length);
      author.writeArticle("test article");
      assert.equal(1, author.articles.length);
    });
  });

  describe("#listArticles", function(){
    it("should list articles", function(){
      var author = new Author("Jim");
      author.writeArticle("a great article");
      assert.equal("Jim has written: a great article", author.listArticles());
    });
  });
});

Now let’s add a test task to package.json:

"scripts": {
  "info": "echo 'npm as a build tool'",
  "lint": "echo '=> linting' && jshint assets/scripts/*.js",
  "test": "echo '=> testing' && mocha test/"
}

npm has a few convenient shortcuts, namely npm test, npm start and npm stop. These are are all aliases for their run equivalents, which means we just need to run npm test to kick mocha into action:

$ npm test -s
=> testing

  Author
    constructor
      ✓ should have a default name
    #writeArticle
      ✓ should store articles
    #listArticles
      ✓ should list articles

  3 passing (5ms)

Pre and Post Hooks

It wouldn’t be very efficient if we were to run our test suite and it bailed straight away because of a syntax error. Luckily npm gives us the pre and post hooks, so if you run npm run test it will first execute npm run pretest and npm run posttest when it finishes. In this case we want to run the lint script before the test script. The following pretest script makes this possible.

"scripts": {
  "info": "echo 'npm as a build tool'",
  "lint": "echo '=> linting' && jshint assets/scripts/*.js",
  "test": "echo '=> testing' && mocha test/",
  "pretest": "npm run lint -s"
}

Imagine we hadn’t have corrected the syntax errors in our script previously. In this case, the above pretest script will fail with a non-zero exit code and the test script won’t run. That is exactly the behavior we want.

$ npm test -s
=> linting
assets/scripts/main.js: line 1, col 1, Use the function form of "use strict".
...
6 errors

With the corrected code in main.js:

=> linting
=> testing

  Author
    constructor
      ✓ should have a default name
    #writeArticle
      ✓ should store articles
    #listArticles
      ✓ should list articles

  3 passing (6ms)

We are in the green !

Code Minification

For this section, we’ll need to add a dist directory to our project, as well as several sub directories and files. This is how the folder structure looks:

   ├── dist
   │   └── public
   │       ├── css
   │       ├── index.html
   │       └── js

The command to recreate this on a Unix machine is:

mkdir -p dist/public/css dist/public/js && touch dist/public/index.html

The contents of index.html is simple.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>npm as a build tool</title>
    <link href='css/main.min.css' rel='stylesheet'>
  </head>
  <body>
    <h2>npm as a build tool</h2>
    <script src='js/main.min.js'></script>
  </body>
</html>

Currently main.js is not minified. This is as it should be, because it is the file we are working in and we need to be able to read it. However, before we upload it to the live server, we need to reduce its size and place it in the dist/public/js directory. To do this we can install the uglify-js package and make a new script.

$ npm install uglify-js --save-dev

We can now make a new minify:js script in package.json:

"scripts": {
  "info": "echo 'npm as a build tool'",
  "lint": "echo '=> linting' && jshint assets/scripts/*.js",
  "test": "echo '=> testing' && mocha test/",
  "minify:js": "echo '=> minify:js' && uglifyjs assets/scripts/main.js -o dist/public/js/main.min.js",
  "pretest": "npm run lint -s"
}

Run it:

$ npm run minify:js -s
=> minify:js

And the script creates a minified version of our file in the right destination. We will do the same for our CSS file using the clean-css package.

$ npm install clean-css --save-dev

And create the minify:css script.

"scripts": {
  "info": "echo 'npm as a build tool'",
  "lint": "echo '=> linting' && jshint assets/scripts/*.js",
  "test": "echo '=> testing' && mocha test/",
  "minify:js": "echo '=> minify:js' && uglifyjs assets/scripts/main.js -o dist/public/js/main.min.js",
  "minify:css": "echo '=> minify:css' && cleancss assets/css/main.css -o dist/public/css/main.min.css",
  "pretest": "npm run lint -s"
}

Let’s run the script.

$ npm run minify:css -s
=> minify:css

Watching for Changes

One of the things that Grunt, Gulp and their ilk are great at, is watching a set of files and re-running a specific task whenever one of those files is detected to have changed. This is particularly useful in cases such as this, as it would be a pain in the neck to re-run the minification scripts manually.

The good news is that you can do that in npm, too, using a package such as watch, which is intended to make managing the watching of file and directory trees easier.

$ npm install watch --save-dev

Then in package.json, you need to specify the tasks to run when a change is detected. In this case JavaScript and CSS minification:

"scripts": {
  ...
  "watch": "watch 'npm run minify:js && npm run minify:css' assets/scripts/ assets/css/"
}

Start the script using:

$ npm run watch

Now, whenever any file in assets/scripts/ or assets/css/ changes, the minification scripts will be called automatically.

Build Script

By now we have several scripts that we can chain together to make a build script which should do the following: linting, testing and minifying. It would, after all, be a pain to have to run these tasks individually time after time. To create this build script, alter the script object in package.json, thus:

"scripts": {
  "info": "echo 'npm as a build tool'",
  "lint": "echo '=> linting' && jshint assets/scripts/*.js",
  "test": "echo '=> testing' && mocha test/",
  "minify:js": "echo '=> minify:js' && uglifyjs assets/scripts/main.js -o dist/public/js/jquery.min.js",
  "minify:css": "echo '=> minify:css' && cleancss assets/css/main.css -o dist/public/css/main.min.css",
  "build": "echo '=> building' && npm run test -s && npm run minify:js -s && npm run minify:css -s",
  "pretest": "npm run lint -s"
}

Running the build script gives us the following output.

$ npm run build -s
=> building
=> linting
=> testing

  Author
    constructor
      ✓ should have a default name
    #writeArticle
      ✓ should store articles
    #listArticles
      ✓ should list articles

  3 passing (6ms)

=> minify:js
=> minify:css

Server Script

After we run our build script it would be nice if we could start a server for our content in dist and check it in the browser. We can do this using the http-server package.

$ npm install http-server -save-dev

We make a server script.

"scripts": {
  ...
  "server": "http-server dist/public/",
}

And now we can run our server.

$ npm run server
Starting up http-server, serving dist/public/ on: http://0.0.0.0:8080
Hit CTRL-C to stop the server
_

Of course the server script can be added to the build script, but I leave that as an exercise for the reader.

Conclusion

Hopefully this article has demonstrated how flexible and powerful npm can be as a build tool. The next time you are starting a new project, try not to reach straight for a tool such as Gulp or Grunt — try to solve your needs by using npm only. You might be pleasantly surprised.

If you have any questions or comments, I’d be glad to hear them in the thread below.

Frequently Asked Questions (FAQs) about npm as a Build Tool

What is the difference between npm and Grunt?

npm (Node Package Manager) and Grunt are both task runners, but they have different approaches. npm is a package manager for the JavaScript programming language and it is the default package manager for the JavaScript runtime environment Node.js. It is used to install, share, and distribute code, and manage dependencies in your projects. On the other hand, Grunt is a JavaScript task runner that automates repetitive tasks like minification, compilation, unit testing, and linting. It uses a command-line interface to run custom tasks defined in a file known as a Gruntfile.

How do I get started with npm as a build tool?

To get started with npm as a build tool, you first need to install Node.js and npm on your computer. Once installed, you can create a new project and initialize it with a package.json file using the command ‘npm init’. This file will contain all the metadata about your project and the dependencies it needs. You can then install packages using ‘npm install’ and run scripts using ‘npm run’.

What is the difference between ‘npm install’ and ‘npm run build’?

npm install’ is a command that installs all the dependencies listed in your package.json file. These dependencies are necessary for your project to run correctly. On the other hand, ‘npm run build’ is a command that runs the ‘build’ script defined in your package.json file. This script usually involves tasks like compiling code, minifying files, or creating production-ready builds of your project.

How can I use Grunt with npm?

To use Grunt with npm, you first need to install Grunt’s command line interface (CLI) globally using ‘npm install -g grunt-cli’. Then, in your project directory, you can install Grunt locally and save it as a devDependency using ‘npm install grunt –save-dev’. You can then create a Gruntfile.js in your project root, where you’ll define your tasks.

What is the role of the package.json file in an npm project?

The package.json file is a crucial part of any npm project. It holds various metadata relevant to the project, including the project’s name, version, description, and author. It also lists the project’s dependencies, which are the packages that your project needs to run. When you run ‘npm install’, npm looks at the package.json file to determine which packages to install.

How can I use npm scripts as a build tool?

npm scripts can be used as a build tool by defining “scripts” in your package.json file. These scripts can automate tasks like starting a server, running tests, or transpiling code. To run a script, you use the ‘npm run’ command followed by the name of the script.

Can I use both npm and Grunt in the same project?

Yes, you can use both npm and Grunt in the same project. npm can be used to manage your project’s dependencies, while Grunt can be used to automate tasks like minification, compilation, and testing. You can even use npm scripts to run your Grunt tasks.

How can I update my npm packages?

To update your npm packages, you can use the ‘npm update’ command. This command will update all the packages listed in your package.json file to the latest version. If you want to update a specific package, you can use ‘npm update ‘ followed by the package name.

What is the purpose of the ‘npm run build’ command?

The ‘npm run build’ command is used to create a production-ready build of your project. This command runs the ‘build’ script defined in your package.json file, which usually involves tasks like compiling code, minifying files, or bundling assets.

How can I troubleshoot common npm issues?

Common npm issues can often be resolved by checking a few key areas. First, ensure that you have the latest version of Node.js and npm installed. Second, check your package.json file for errors or missing dependencies. If you’re having trouble installing a package, try clearing the npm cache with ‘npm cache clean’ or check the npm registry status. If all else fails, searching for the error message online can often lead to solutions from other developers who have faced the same issue.