Using Grunt + Bower with Laravel and Bootstrap

One day I got fed up with the way I used to compile LESS and searched for a more automated way. As I use Laravel for most of my projects I tried Basset asset manager. It worked nicely for some time than it started to have a mind of it's own. I switched from it, but took note of the concept to store "raw" assets in the app folder. I tried Guard next; couldn't get the LESS compiler to work. Finally I tried having another shot with Grunt, even though the first one had scared me.
And wow! Revelation! I realized I've been suffering this whole time without even knowing it.

So, I'll try showing here how I automated my frontend tasks (less compilation, concatenation and minification) and handled test execution on a Laravel project using Twitter Bootstrap. Even more, we'll make Grunt watch for specific folders, ready to execute specific tasks, on save and reload our browser to see the changes (Alt+Shift, F5 Nevermore!)

What is Grunt

Official definition: Grunt: The JavaScript Task Runner

Grunt is basically a tool that automates your frontend tasks, but it goes beyond that if you want it. It can upload your changed files to CDN networks, copy files to production environment, optimize images and more. There are inumerous plugins for all these functions and if you can't find one that suits your needs, you can always write your own.

Installation

- Grunt

Grunt runs on Node.js, so if you don't have npm installed already, go ahead and install it.

To install Grunt's command line interface run:

npm install -g grunt-cli

With the flag -g you installed it globally and now you can access it from anywhere on your system.

To initialize a new Grunt project from your project's directory run

npm init

and follow the instructions. The command will create your package.json file, necessary for any npm project.


{
  "name": "gruntRocks",
  "version": "0.0.1",
  "description": "Using Grunt with Laravel and Bootstrap",
  "main": "Gruntfile.js",
  "repository": {
    "type": "git",
    "url": "https://github.com/elena-kolevska/grunt-laravel.git"
  },
  "keywords": [
    "Laravel",
    "Grunt",
    "Bootstrap",
    "elenakolevska.com"
  ],
  "author": "Elena Kolevska",
  "license": "BSD",
  "bugs": {
    "url": "https://github.com/elena-kolevska/grunt-laravel/issues"
  }
}

Finally, let's install grunt and some plugins as dependencies. We'll use:

  • "grunt-contrib-concat": For concatenation;
  • "grunt-contrib-less": For LESS compilation
  • "grunt-contrib-uglify": For javascript minification
  • "grunt-phpunit": For running PhpUnit tests
  • "grunt-contrib-watch": To watch files for changes

In your terminal run:

npm install grunt --save-dev
npm install grunt-contrib-concat --save-dev
npm install grunt-contrib-less --save-dev
npm install grunt-contrib-uglify --save-dev
npm install grunt-contrib-watch --save-dev
npm install grunt-phpunit --save-dev

That will install the dependencies and because we defined the --save-dev flag it will add them to the package.json file.

 

- Bower

Installing bower couldn't be simpler. Just go:

npm install -g bower

And you have it.

 

- Frontend dependencies

For this tutorial we'll use Bootstrap and jQuery so let's install them with Bower:

You could use only bower install bootstrap and that would work, but if you want to save a list of dependencies go ahead and create a bower.json file containing only a project name:

{
  "name": "gruntRocks"
}

Now run:

bower install bootstrap -S

The -S flag will save the dependency in the bower.json file and later you can just run bower install to replicate the exact front-end dependencies of your project. If you're not one of those guys paranoid about github just dying one day and our poor selves left without our packages, you can freely add bower_components to your .gitignore file and just track bower.json.

I know you noticed already; I'm forgetting about jQuery. But actually we already got it. In its bower.json file Bootstrap defines that it depends on jQuery, so it got automatically pulled in together with Bootstrap.

The default installation directory is bower_components. If you wish, you can change that in a .bowerrc file in the root of the project, as well as many other configuration options, but that isn't really the key point of this tutorial, so let's keep our focus on grunt for now and if you'd like to play around with bower some more later, go ahead and check out their web site

Here's how our components folder turned out (only the parts that concern us):

/bower_components
    /bootstrap
      /dist
        /js
        - bootstrap.js
        - bootstrap.min.js
      /less
      - alerts.less
      - badges.less
      - bootstrap.less
      ...
      ...
      - variables.less
      - wells.less
    /jquery
      - jquery.js
      - jquery.min.js

Configuring Gruntfile.js

Buckle up, here begins the fun part! Let's create the configuration file Gruntfile.js in root. This is its basic structure:


    //Gruntfile
    module.exports = function(grunt) {

    //Initializing the configuration object
      grunt.initConfig({

        // Task configuration
        concat: {
          //...
        },
        less{
          //...
        },
        uglify{
          //...
        },
        phpunit{
          //...
        },
        watch{
          //...
        }
      });

    // Plugin loading

    // Task definition

  };

The stylesheets

Let's create the following filestructure:

/app
  /assets
    /javascript
      - backend.js
      - frontend.js
    /stylesheets
      - backend.less
      - base.less
      - fonts.less
      - frontend.less
      - variables.less

The file /app/assets/stylesheets/frontend.less:

@import 'base.less'; 
//styles specific for the frontend 
// ... 

The file /app/assets/stylesheets/backend.less:

@import 'base.less'; 
//styles specific for the backend 
// ... 

The stylesheets that are common to both the backend and front end are in base.less. That's also the file where we include all the components from Bootstrap we need. It will go something like this:

@import "variables.less";
@import "../../../bower_components/bootstrap/less/mixins.less";

// Reset
@import "../../../bower_components/bootstrap/less/normalize.less";
@import "../../../bower_components/bootstrap/less/print.less";

// Core CSS
@import "../../../bower_components/bootstrap/less/scaffolding.less";
@import "../../../bower_components/bootstrap/less/type.less";
@import "../../../bower_components/bootstrap/less/code.less";
/*
...
...
*/

And let's set up their tasks:

    less: {
        development: {
            options: {
              compress: true,  //minifying the result
            },
            files: {
              //compiling frontend.less into frontend.css
              "./public/assets/stylesheets/frontend.css":"./app/assets/stylesheets/frontend.less",
              //compiling backend.less into backend.css
              "./public/assets/stylesheets/backend.css":"./app/assets/stylesheets/backend.less"
            }
        }
    },

To run only this single task run grunt less in the command line.

The javascript

This one is pretty clear, first we're concatenating jquery.js, bootstrap.js and frontend.js in a single frontend.js file that will live in the public directory and than we repeat the same thing for the backend javascript file backend.js.

    concat: {
      options: {
        separator: ';',
      },
      js_frontend: {
        src: [
          './bower_components/jquery/jquery.js',
          './bower_components/bootstrap/dist/js/bootstrap.js',
          './app/assets/javascript/frontend.js'
        ],
        dest: './public/assets/javascript/frontend.js',
      },
      js_backend: {
        src: [
          './bower_components/jquery/jquery.js',
          './bower_components/bootstrap/dist/js/bootstrap.js',
          './app/assets/javascript/backend.js'
        ],
        dest: './public/assets/javascript/backend.js',
      },
    },

To run only this single task run grunt:concat, grunt concat:js_frontend or grunt concat:js_backend in the command line.

Note that when installing packages, you usually get their minified versions too, but we used the extended ones, cause we want to take a look how minification is done:

    uglify: {
      options: {
        mangle: false  // Use if you want the names of your functions and variables unchanged
      },
      frontend: {
        files: {
          './public/assets/javascript/frontend.js': './public/assets/javascript/frontend.js',
        }
      },
      backend: {
        files: {
          './public/assets/javascript/backend.js': './public/assets/javascript/backend.js',
        }
      },
    },

To run only this single task run grunt:uglify, grunt uglify:frontend or grunt uglify:backend in the command line.

The tests

Grunt can automatically run your tests in Laravel, but for that, we'll need to install PhpUnit. Let's add it to our composer.json.

"phpunit/phpunit": "3.7.*"

and run composer update.

And the task definition:

    phpunit: {
        classes: {
        },
        options: {
        }
    },

It looks strange, but it will function, even though we're not configuring anything. That's because phpunit picks up the configuration from /phpunit.xml. But to stay consistent, here's a really basic setup:

        phpunit: {
            classes: {
                dir: 'app/tests/'   //location of the tests
            },
            options: {
                bin: 'vendor/bin/phpunit',
                colors: true
            }
        },

To run only this single task run grunt phpunit in the command line.

Watching the filesystem for changes

Grunt's superpower is running tasks without having to do many things. It watches your filesystem for changes and it only needs your instructions to know what to do and when to do it.

We already defined 4 tasks: concat, less, uglify and phpunit. You could run every one of them separately, if you want, but to fully harness the power of Grunt and automate pur project let's put the bits and pieces together and define the big bad watch task that will watch certain files and run determinate tasks.

    watch: {
        js_frontend: {
          files: [
            //watched files
            './bower_components/jquery/jquery.js',
            './bower_components/bootstrap/dist/js/bootstrap.js',
            './app/assets/javascript/frontend.js'
            ],   
          tasks: ['concat:js_frontend','uglify:frontend'],     //tasks to run
          options: {
            livereload: true                        //reloads the browser
          }
        },
        js_backend: {
          files: [
            //watched files
            './bower_components/jquery/jquery.js',
            './bower_components/bootstrap/dist/js/bootstrap.js',
            './app/assets/javascript/backend.js'
          ],   
          tasks: ['concat:js_backend','uglify:backend'],     //tasks to run
          options: {
            livereload: true                        //reloads the browser
          }
        },
        less: {
          files: ['./app/assets/stylesheets/*.less'],  //watched files
          tasks: ['less'],                          //tasks to run
          options: {
            livereload: true                        //reloads the browser
          }
        },
        tests: {
          files: ['app/controllers/*.php','app/models/*.php'],  //the task will run only when you save files in this location
          tasks: ['phpunit']
        }
      }

So, you're probably already guessing: every time one of the defined frontend javascript files is saved, Grunt will run the tasks concat:js_frontend and uglify:frontend. Same thing with the tasks js_backend, less and tests and their appropriate files. Even more, because we defined livereload: true on the js_frontend, js_backend and less tasks, the browser will reload every time those tasks are runned. Well, actually.. sorry.. it wont really, not yet.. there's one last thing you'll need to do: install the Live reload extension and activate it for the tab your app is running in.

Finalizing

The only thing left now is load the necessary npm plugins and register the default task:

        
    // // Plugin loading
    grunt.loadNpmTasks('grunt-contrib-concat');
    grunt.loadNpmTasks('grunt-contrib-watch');
    grunt.loadNpmTasks('grunt-contrib-less');
    grunt.loadNpmTasks('grunt-contrib-uglify');
    grunt.loadNpmTasks('grunt-phpunit');


    // Task definition
    grunt.registerTask('default', ['watch']);

The task "default" is the one that will be executed when we run only grunt in the terminal. In our case, that will run the task watch whitch, as we said, will watch our files for changes and execute the appropriate tasks on save.

Finally, the complete Gruntfile.js

module.exports = function(grunt) {

  //Initializing the configuration object
    grunt.initConfig({

      // Task configuration
    less: {
        development: {
            options: {
              compress: true,  //minifying the result
            },
            files: {
              //compiling frontend.less into frontend.css
              "./public/assets/stylesheets/frontend.css":"./app/assets/stylesheets/frontend.less",
              //compiling backend.less into backend.css
              "./public/assets/stylesheets/backend.css":"./app/assets/stylesheets/backend.less"
            }
        }
    },
    concat: {
      options: {
        separator: ';',
      },
      js_frontend: {
        src: [
          './bower_components/jquery/jquery.js',
          './bower_components/bootstrap/dist/js/bootstrap.js',
          './app/assets/javascript/frontend.js'
        ],
        dest: './public/assets/javascript/frontend.js',
      },
      js_backend: {
        src: [
          './bower_components/jquery/jquery.js',
          './bower_components/bootstrap/dist/js/bootstrap.js',
          './app/assets/javascript/backend.js'
        ],
        dest: './public/assets/javascript/backend.js',
      },
    },
    uglify: {
      options: {
        mangle: false  // Use if you want the names of your functions and variables unchanged
      },
      frontend: {
        files: {
          './public/assets/javascript/frontend.js': './public/assets/javascript/frontend.js',
        }
      },
      backend: {
        files: {
          './public/assets/javascript/backend.js': './public/assets/javascript/backend.js',
        }
      },
    },
    phpunit: {
        classes: {
        },
        options: {
        }
    },
    watch: {
        js_frontend: {
          files: [
            //watched files
            './bower_components/jquery/jquery.js',
            './bower_components/bootstrap/dist/js/bootstrap.js',
            './app/assets/javascript/frontend.js'
            ],   
          tasks: ['concat:js_frontend','uglify:frontend'],     //tasks to run
          options: {
            livereload: true                        //reloads the browser
          }
        },
        js_backend: {
          files: [
            //watched files
            './bower_components/jquery/jquery.js',
            './bower_components/bootstrap/dist/js/bootstrap.js',
            './app/assets/javascript/backend.js'
          ],   
          tasks: ['concat:js_backend','uglify:backend'],     //tasks to run
          options: {
            livereload: true                        //reloads the browser
          }
        },
        less: {
          files: ['./app/assets/stylesheets/*.less'],  //watched files
          tasks: ['less'],                          //tasks to run
          options: {
            livereload: true                        //reloads the browser
          }
        },
        tests: {
          files: ['app/controllers/*.php','app/models/*.php'],  //the task will run only when you save files in this location
          tasks: ['phpunit']
        }
      }
    });

  // Plugin loading
  grunt.loadNpmTasks('grunt-contrib-concat');
  grunt.loadNpmTasks('grunt-contrib-watch');
  grunt.loadNpmTasks('grunt-contrib-less');
  grunt.loadNpmTasks('grunt-contrib-uglify');
  grunt.loadNpmTasks('grunt-phpunit');

  // Task definition
  grunt.registerTask('default', ['watch']);

};

The complete code (without the whole Laravel installation) is available on github. If you'd like to contribute, feel free to issue a pull request.

comments powered by Disqus