super big tree

Introduction to grunt.js and npm scripts, and choosing between the two.

As our javascript projects get more complex, we need ways to automate all the repetitive tasks that come along with building applications.

We need to run tests, run jslint, create builds, run a development server, deploy code, and otherwise manage our projects.

Grunt.js is great for these things.

Hold it.

Before you jump to grunt for all your build-step and task-automation needs, consider if the npm scripts feature might suit your needs.

Maybe you only need to run a development server for you html/css/javascript project.

Instead of jumping to grunt, first create a package.json file if you don’t have one already:

npm init

This will ask you a few questions about your project. Answer and you’ll get a package.json file!

It will probably look like this:

{
  "name": "example",
  "version": "0.0.0",
  "description": "example project",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "sethvincent",
  "license": "BSD"
}

You can revise the main value to be whatever makes sense for your project. If this won’t be released on npm, you might not even need main.

Now, check out the scripts section. There’s already test, but it needs to have an actual command associated with it. Change the error message to whatever command you use to run tests.

I might change it to something like this:

"scripts": {
  "test": "node test.js"
}

Where test.js would contain a set of tests written in tape.

To run the tests, you can run this command:

npm test

That doesn’t gain a whole lot of simplicity over just running node test.js, but when the command for running tests is more complicated, being able to run npm test instead helps out a lot.

A development server

Let’s use a simple development server designed to work with browserify named beefy.

Install beefy as a development dependency:

npm install beefy --save-dev

Add a start script to your package.json:

"scripts": {
  "test": "node test.js",
  "start": "beefy index.js:bundle.js --live"
}

Now we start to see how npm scripts can be useful. That beefy command is just long enough that it’s useful to have npm start as an alias for our development server.

Just to prove it works, create an index.js file and pop in a short console.log statement:

console.log('it works!');

Now run npm start, and you’ll see output that looks like this:

> example@0.0.0 start /Users/sethvincent/workspace/bookery/tmp
> beefy index.js:bundle.js --live

listening on 9966

Now you can go to http://localhost:9966 to see if the console.log statement worked.

It did! (right?).

You might be saying, “hey, I don’t have an index.html file what is this magic.”

If there isn’t an index.html file in the directory you’re in when you run beefy, it’ll serve a really basic index.html file so your javascript can run.

Add another statement to your index.js file:

document.write('served.');

You’ll see that without reloading the page the string served is now displayed in your index.html file.

Arbitrary scripts

There are a few default script names you can hook into and run your own scripts using npm aside from test and start. To see a list of them, read the npm-scripts documentation.

You can also run arbitrary scripts. Let’s add a pizza script.

```
"scripts": {
  "test": "node test.js",
  "start": "beefy index.js:bundle.js --live",
  "pizza": "node pizza.js"
}
```

Let’s pretend pizza.js automates the process of ordering a pizza online from your favorite pizza place (shit, this should be a thing).

To run the command we prepend our script name with run, like this:

npm run pizza

You can add whatever scripts you want. You can even set up a master script that runs a bunch of your npm scripts! Chek it out:

```
"scripts": {
  "test": "node test.js",
  "start": "beefy index.js:bundle.js --live",
  "pizza": "node pizza.js",
  "everything": "npm run pizza && npm test && npm start"
}
```

In the terminal execute this command:

npm run everything

And you just ordered pizza, ran tests, and started your development server! That’s pretty good. That easily rivals the most basic grunt.js implementations.

So, that gives you a sense of when to use npm scripts. Use npm scripts when your requirements are simple. You may find it will work for many situations.

Things get complicated

npm scripts are great for simple projects, and sometimes even for complex projects if your write your own build tools.

But for bigger projects I tend to use grunt. I’ve been experimenting with using grunt to generate static sites similar to jekyll.

Let’s try out an example that includes: - generating html from ejs templates - bundling javascript with browserify - including javascript from bower in our browserify bundle - a development server using connect - and deploying the generated code to github pages

First you need to install the grunt command line tool:

npm install -g grunt-cli

Next, we’ll run npm init again to set up the project.

We have a bunch of development dependencies for this project, so add those to the package.json file:

  "devDependencies": {
    "grunt": "~0.4.1",
    "matchdep": "~0.1.2",
    "grunt-contrib-clean": "~0.4.1",
    "grunt-contrib-concat": "~0.3.0",
    "grunt-contrib-connect": "~0.3.0",
    "grunt-contrib-copy": "~0.4.1",
    "grunt-contrib-watch": "~0.4.4",
    "grunt-browserify": "~1.2.0",
    "grunt-gh-pages": "~0.6.0",
    "grunt-ejs": "~0.1.0",
    "debowerify": "~0.1.3"
  }
}

Run npm install in the terminal to install those dependencies.

Now, before you run the grunt command, you need a file named Gruntfile.js.

Create the file and add this to it:

module.exports = function(grunt) {
  grunt.initConfig({

    clean: ['dist'],

    ejs: {
      all: {
        options: {
          // site-wide vars here
        },
        src: ['**/*.ejs', '!node_modules/**/*', '!_*/**/*'],
        dest: 'dist/',
        expand: true,
        ext: '.html',
      },
    },

    copy: {
      all: {
        src: ['*.css', '*.html', 'images/**/*', 'img/**/*', '!Gruntfile.js'],
        dest: 'dist/',
      },
    },

    browserify: {
      all: {
        src: 'app.js',
        dest: 'dist/app.js'
      },
      options: {
        transform: ['debowerify']
      }
    },

    connect: {
      options: {
        port: process.env.PORT || 3131,
        base: 'dist/',
      },

      all: {},
    },

    watch: {
      options: {
        livereload: true
      },

      html: {
        files: '<%= ejs.all.src %>',
        tasks: ['ejs'],
      },

      js: {
        files: '<%= browserify.all.src %>',
        tasks: ['browserify'],
      },

      assets: {
        files: ['assets/**/*', '*.css', '*.js', 'images/**/*', 'img/**/*', '!Gruntfile.js'],
        tasks: ['copy'],
      }
    },

    'gh-pages': {
      options: {
        base: 'dist/'
      },
      src: ['**/*']
    }
  });

  require('matchdep').filterDev('grunt-*').forEach(grunt.loadNpmTasks);

  grunt.registerTask('default', ['clean', 'ejs', 'browserify', 'copy']);

  grunt.registerTask('server', ['default', 'connect', 'watch']);

  grunt.registerTask('deploy', ['default', 'gh-pages']);

};

Let’s run through this a chunk at a time:

This is the essential boilerplate to get started writing a Gruntfile:

module.exports = function(grunt) {
  grunt.initConfig({
    // config goes here.
  });
};

Next, you also need to load the grunt plugins being used in your gruntfile. Here I’m using a module that checks the package.json filed for any module that starts with grunt- and loads it as a grunt plugin.

require(‘matchdep’).filterDev(‘grunt-*’).forEach(grunt.loadNpmTasks);

Here we register tasks:

grunt.registerTask('default', ['clean', 'ejs', 'browserify', 'copy']);

grunt.registerTask('server', ['default', 'connect', 'watch']);

grunt.registerTask('deploy', ['default', 'gh-pages']);

default, server, and deploy are all commands now that can be run like this:

grunt
grunt server
grunt deploy

The default task is run when you run the grunt command on its own. Each of those tasks are essentially a list of things that need to be done.

Let’s take a look at each of those items in detail.

clean deletes the dist folder so that it can be built fresh using other config tasks

clean: ['dist'],

ejs takes care of compiling all the ejs templates and putting them in the dist folder:

ejs: {
  all: {
    options: {
      // site-wide vars here
    },
    src: ['**/*.ejs', '!node_modules/**/*', '!_*/**/*'],
    dest: 'dist/',
    expand: true,
    ext: '.html',
  },
},

copy simply copies the static files that don’t need to be processed and puts them into the dist folder:

copy: {
  all: {
    src: ['*.css', '*.html', 'images/**/*', 'img/**/*', '!Gruntfile.js'],
    dest: 'dist/',
  },
},

browserify bundles the javascript, and we’re using the transform option so that we can require javascript components from bower in our node-style code:

browserify: {
  all: {
    src: 'app.js',
    dest: 'dist/app.js'
  },
  options: {
    transform: ['debowerify']
  }
},

connect runs the development server:

connect: {
  options: {
    port: process.env.PORT || 3131,
    base: 'dist/',
  },

  all: {},
},

watch keeps track of all the relevant files, and when they are saved, reruns the necessary tasks to move the files into the dist folder.

watch: {
  options: {
    livereload: true
  },

  html: {
    files: '<%= ejs.all.src %>',
    tasks: ['ejs'],
  },

  js: {
    files: '<%= browserify.all.src %>',
    tasks: ['browserify'],
  },

  assets: {
    files: ['assets/**/*', '*.css', '*.js', 'images/**/*', 'img/**/*', '!Gruntfile.js'],
    tasks: ['copy'],
  }
},

To get live-reloading of the page working correctly we embed this snippet into the html:

<script id="live-reload" src="http://localhost:35729/livereload.js"></script>

gh-pages takes whatever is in our dist folder, copies it to a gh-pages branch, and pushes it to our github repository.

'gh-pages': {
  options: {
    base: 'dist/'
  },
  src: ['**/*']
}

I like that one a lot.

So, you can see that doing all the things that grunt is doing for our static site generator would take a lot of work to replicate using only npm scripts. The best part is that I didn’t have to write any of the grunt plugins that handle the real hard work, I just plugged them together in the Gruntfile based on the documentation of each plugin.

Here’s a list of the plugins and npm modules I used:

Grunt plugins:

Other modules:

I based the grunt example on a yeoman generator I’m working on: grunt-static-site. Please try it out and tell me if you come up against any issues in the issues queue.

  1. superbigtree posted this