Using Grunt to Auto-Restart node.js with File Watchers

Written by Russell on . Posted in App Dev, Deployment, Front End, Productivity, Software

Preprocessors have become a very important part of the development life cycle. In the past we would just write some HTML, JavaScript and CSS with a backend and deploy a website.  Now for better speed, development experience and more manageable outcomes we have a multitude of languages that compile into these standards, e.g: CoffeeScript–> JavaScript, LESS –> CSS, Jade –> HTML… Then there is JS, CSS and HTML compression and minification after that.

Creating a development workflow to manage all of these transformations can be a very daunting task at the beginning of a new project. The complexity of the workflow can can also make or break a project. When ever I try to solve a problem I always try to look at the difficulty curve of reproduction and repeatability. In other words, it shouldn’t be difficult to setup a new developer on the project and it should be easy to perform 1k times a day without a work-performance loss.

Grunt has become a very important part of our  development life cycles. For compiled projects such as Java, adding in Grunt from ANT or Maven is relatively simple to in-line. For our node.js projects we wanted to be able to not only run the node server, but we wanted to be able to auto-restart the process while also auto-building resource files like LESS and JS files. Below you will find a lengthy Grunt file with commentary interlaced. This should help you get started with your own development environment.

'use strict';

module.exports = function(grunt) {
    grunt.initConfig({
        /**
            The concurrent task will let us spin up all of required tasks. It is very 
            important to list the 'watch' task last because it is blocking and nothing 
            after it will be run.
        **/
        concurrent: {
            dev: ["less:dev", "nodemon", "watch"],
            options: {
                logConcurrentOutput: true
            }
        },

        /**
            The nodemon task will start your node server. The watch parameter will tell 
            nodemon what files to look at that will trigger a restart. Full grunt-nodemon 
            documentation
        **/
        nodemon: {
            dev: {
                script: 'index.js',
                options: {
                    /** Environment variables required by the NODE application **/
                    env: {
                          "NODE_ENV": "development"
                        , "NODE_CONFIG": "dev"
                    },
                    watch: ["server"],
                    delay: 300,

                    callback: function (nodemon) {
                        nodemon.on('log', function (event) {
                            console.log(event.colour);
                        });

                        /** Open the application in a new browser window and is optional **/
                        nodemon.on('config:update', function () {
                            // Delay before server listens on port
                            setTimeout(function() {
                                require('open')('http://127.0.0.1:8000');
                            }, 1000);
                        });

                        /** Update .rebooted to fire Live-Reload **/
                        nodemon.on('restart', function () {
                            // Delay before server listens on port
                            setTimeout(function() {
                                require('fs').writeFileSync('.rebooted', 'rebooted');
                            }, 1000);
                        });
                    }
                }
            }
        },

        /**
            Watch the JS and LESS folders for changes. Triggering 
            fires off the listed tasks
        **/
        watch: {
            js: {
                files: ["client/resources/less/**/*.js"],
                tasks: ['copy:dev:custom'],
                options: { nospawn: true, livereload: true }
            },
            less: {
                files: ["client/resources/less/**/*.less"],
                tasks: ['less'],
                options: { nospawn: true, livereload: true }
            }
        },

        /** 
            Less task to compile LESS into CSS.
            Different options for dev and prod
        **/
        less: {
            dev: {
                options: {
                    compress: false,
                    yuicompress: false,
                    strictMath: true,
                    strictUnits: true,
                    strictImports: true
                },
                files: lessFiles
            }, 
            prod: {
                options: {
                    compress: true,
                    yuicompress: true,
                    strictMath: true,
                    strictUnits: true,
                    strictImports: true
                },
                files: lessFiles
            }
        },

        /**
            Used for production mode, minify and uglyfy the JavaScript Output
        **/
        uglify: {
            prod: {
                options: {
                    mangle: true,
                    compress: true,
                    sourceMap: true,
                    drop_console: true
                },
                files: {
                    'client/public/js/main.js': ['client/resources/js/main.js']
                }
            }
        },

        /**
            This copy tasks has two parts. The libraries will be rarely updated and are 
            only copied on startup. The custom sub-task will copy over application specific 
            JS since it doesn't need a preprocessor in dev
        **/
        copy: {
            dev: {
                custom: {
                    files: [
                        {
                            src: ["client/public/js/*.js"], 
                            dest: "client/public/js", 
                            expand: true, 
                            flatten: true
                        }
                    ]
                },
                libs: {
                    files: [
                      /** 
                        Array of file objects that reference bower libs }}
                      **/
                    ]
                }
            }
        }
    });

    /**
        Load all the GRUNT tasks
    **/
    grunt.loadNpmTasks("grunt-nodemon");
    grunt.loadNpmTasks("grunt-concurrent")
    grunt.loadNpmTasks("grunt-contrib-copy")
    grunt.loadNpmTasks("grunt-contrib-less")
    grunt.loadNpmTasks("grunt-contrib-watch");
    grunt.loadNpmTasks("grunt-contrib-uglify");

    /**
        Register tasks allowing you to run:
            grunt
            grunt run
            grun dev
            grun prod
    **/
    grunt.registerTask("run", ["concurrent:dev"]);
    grunt.registerTask("default", ["concurrent:dev"]);

    grunt.registerTask("dev", ["less:dev", "copy:dev"]);
    grunt.registerTask("prod", ["uglify:prod", "less:prod"]);
};

You will need to add a few requirements to your packages.json file to pull in the new requrements

    "devDependencies": {
          "open": "*"
        , "grunt-nodemon": "*"
        , "grunt-concurrent": "*"
        , "grunt-contrib-copy": "*"
        , "grunt-contrib-less": "*"
        , "grunt-contrib-watch": "*"
        , "grunt-contrib-uglify": "*"
    }

Russell

I am a rich internet application developer with 15 years of experience as a technical and team leader. I have extensive experience with enterprise level application development and project management with a strong proficiency in a wide variety of languages and technologies. My development focus of recent has been on delivering high end rich UI applications by pushing the limits of both front end and server side technologies such as Node.JS, Java and Python.