Stop grunting and gulping and just use NPM

May 2, 2015

I build websites for a living and I am constantly looking for ways to make my life easier. Code snippets, shortcuts, automation ... anything that can save me time and reduce doing the same task multiple times is worth consideration to be included in my typical workflow. One thing that most web developers have added to their tool box is a task runner, a process for automating tasks like minifying javascript or compiling SASS to css. Grunt and Gulp are probably the two most popular task running options right now (but there are many others). I have tried them both and found while useful they felt bloated and cumbersome for what I (and probably most website developers) need which is a quick and easy way to build javascript and css. Enter NPM.

Grunt and Gulp both use NPM for installation and dependency management which means if you are using Grunt or Gulp you have NPM running on your development machine. NPM has a sometimes overlooked option, 'scripts', that allows you to define tasks to run, hmmm sounds familiar. Pretty much any plugin for Grunt or Gulp has a corresponding non-g[runt|ulp] version. See where I'm going?

I did some digging and came across some super helpful posts about using NPM for task running, here, here and here.

Without much effort I was able to quickly reduce this package.json and gruntfile.js :

Before

Package.json

<code>
{
    "name" : "website",
    "version" : "0.1.0",
    "author" : "Derrick Grigg",
    "private" : true,
    "devDependencies": {
        "grunt": "~0.4.0",
        "grunt-contrib-compass": "~1.0.1",
        "grunt-contrib-concat": "~0.3.0",
        "grunt-contrib-jshint": "~0.8.0",
        "grunt-contrib-uglify": "~0.2.7",
        "grunt-contrib-watch": "~0.5.3",
        "grunt-yui-compressor": "^0.3.3",
        "grunt-contrib-copy": "~0.7.0"
    }
}
</code>

Gruntfile.js

<code>
'use strict';
module.exports = function (grunt) {
   grunt.initConfig({
      pkg: grunt.file.readJSON('package.json'),
      concat: {
         options: {
            separator: '\r\n'
         },
         main : {
            src: [
               'src/js/vendor/*.js',              
               'src/js/*.js',],
            dest: 'html/js/main.js'
         }
      },
      copy : {
         main : {
            files : [
               {src: 'src/js/vendor/jquery-1.11.0.min.js', dest: 'html/js/jquery-1.11.0.min.js'},
               {src: 'src/js/vendor/modernizr.min.js', dest: 'html/js/modernizr.min.js'},
               {src: 'src/js/utils.js', dest: 'html/js/utils.js'}
            ]
         }
      },
      uglify: {
         options: {
            banner: '/*! <%= pkg.name %> <%= grunt.template.today("dd-mm-yyyy") %> */\n'
         },
         dist: {
            files: {
               'html/js/main.min.js': ['<%= concat.main.dest %>'],
               'html/js/utils.min.js': 'src/js/utils.js'
            }
         }
      },
      compass: {
         dist: {
            options: {
               config: 'config.rb'
            }
         }
      },
      cssmin: {
         dist: {
            files : {
               'html/css/styles.min.css': ['html/css/styles.css']
            }
         }
      },
      watch: {
              js: {
                  files: ['src/js/**/*.js'],
                 tasks: ['concat', 'uglify']
              },
              css: {
                  files: ['html/css/**/*.css'],
                   tasks: ['compass', 'cssmin']
              }
      }
   });
   grunt.loadNpmTasks('grunt-contrib-concat');
   grunt.loadNpmTasks('grunt-contrib-uglify');
   grunt.loadNpmTasks('grunt-contrib-compass');
   grunt.loadNpmTasks('grunt-yui-compressor');
   grunt.loadNpmTasks('grunt-contrib-copy');
   grunt.loadNpmTasks('grunt-contrib-watch');
   grunt.registerTask('default', []);
   grunt.registerTask('buildjs', ['concat', 'uglify', 'copy'])
   grunt.registerTask('buildcss', ['compass', 'cssmin'])
};
</code>

After

Into this single package.json which handles everything

<code>
{
  "name": "Website",
  "version": "0.0.0",
  "author": "Derrick Grigg",
  "main": "index.js",
  "dependencies": {
    "uglify-js": "latest",
    "uglifycss": "latest",
    "onchange": "latest",
    "parallelshell": "latest"
  },
  "scripts": {
    "js:compile:vendor" : "uglifyjs src/js/vendor/*.js -b -o httpdocs/js/vendor/plugins.js",
    "js:compile" : "uglifyjs src/js/main.js src/js/site/*.js -b -o httpdocs/js/main.js",
    "js:watch" : "onchange 'src/js/**/*' -- npm run js:compile",
    "css:compile" : "compass compile && uglifycss httpdocs/css/styles.css > httpdocs/css/styles.min.css",
    "css:watch" : "onchange 'src/sass/**/*' -- npm run css:compile",
    "build:watch": "parallelshell 'npm run css:watch' 'npm run js:watch'"
  }
}
</code>

Talk about short and sweet! Both versions do the exact same thing, watch a javascript and a css (sass) directory for changes and then compile, merge and minify the files for use on a website. The NPM version is short, sweet and to the point. No extraneous modules to load, no extra setup. It's fast, like Gulp, because it uses I/O streams and it's easy to setup and use, like Grunt. Since it uses any package on NPM you don't need to wait for a g[runt|ulp] wrapper, just plug and play.

For me it's all about time and effort and going with NPM is a win on both fronts. Until I run across a project that requires a more complex solution that NPM scripts can't handle this will be my default task running setup, KISS.