Using Laravel, Grunt, Bootstrap and LESS
I get this idea from Elena Kolevska where you get introduce with Grunt - The javascript task runner. This guide got little more detailed and fits my own MAMP vhosts installation.
I always wondered how to automate things. Usually work with Mac GUI tools LESS, or SimpLESS to do the converting, but everytime I am doing it, I felt I lost my precious seconds. It's going to add up during the day.
Then watched some screencasts about Grunt. And it requires you to install NodeJS. I told myself: “Why that extra software I probably don’t need it anyway? I want to save my system resources!”. Well do it, you’re going to love it.
This guide I’ll try to show some automated 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!)
Install Laravel with Composer
Head to: https://getcomposer.org/download/
or use this
curl -sS https://getcomposer.org/installer | php
You can shorten the command
composer
instead ofphp composer.phar
withalias
Typealias composer='php composer.phar'
composer create-project laravel/laravel --prefer-dist
or
composer create-project laravel/laravel laravel 4.2 --prefer-dist
This guide is written for Laravel 4
Goto laravel directory and run
composer update
afterwards to make sure Laravel dependencies are updated
Goto laravel directory and run
php artisan key:generate
afterwards to make sure you generate a key in
./laravel/app/config/app.php
Install NodeJS and others once
You may skip this if you have done this.
It's really great and small for the tasks you're going to love.
Head to: http://nodejs.org/
Make sure you install with sudo
command
Also install Grunt CLI and Bower
npm install -g grunt-cli
npm install -g bower
With the flag -g
you installed it globally and now you can access it from anywhere on your system.
Grunt - The JavaScript Task Runner
Bower - A package manager for the web
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.
Initialize a Grunt project
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
entry point: Gruntfile.js
test command: Enter
git repository: https://github.com/HariantoAtWork/grunt-laravel.git
keywords: Laravel, Grunt, Bootstrap, LESS, mdstn.com
author: Harianto van Insulinde
license: BSD
Is this ok? (yes): Enter
{
"name": "gruntRocks",
"version": "0.0.1",
"description": "Using Grunt with Laravel and Bootstrap",
"main": "Gruntfile.js",
"repository": {
"type": "git",
"url": "https://github.com/HariantoAtWork/grunt-laravel.git"
},
"keywords": [
"Laravel",
"Grunt",
"Bootstrap",
"mdstn.com"
],
"author": "Harianto van Insulnde",
"license": "BSD",
"bugs": {
"url": "https://github.com/HariantoAtWork/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
Or use this single command:
npm install grunt grunt-contrib-concat grunt-contrib-less grunt-contrib-uglify grunt-contrib-watch grunt-phpunit --save-dev
If you already install this dependency before, add this extra flag:
--cache-min 999999
.
That will install the dependencies and because we defined the --save-dev
flag it will add them to the package.json file.
Bower
Bower - A package manager for the web
Set path Bower dependencies installation
After we’ve done installing Laravel with Composer while ago. We’re going to set up the path for Bower dependencies before we continue or it will install in the default directory bower_components. First we must create a file .bowerrc.
with
{
"directory": "laravel/public/assets/vendor"
}
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.
Read more information about bower on http://bower.io
Here's how our components folder turned out (only the parts that concern us):
/ laravel/public/assets/vendor
/ 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({
// Paths variables
paths: {
// Development where put LESS files, etc
assets: {
css: './laravel/public/assets/stylesheets/',
js: './laravel/public/assets/javascripts/',
vendor: './laravel/public/assets/vendor/'
},
// Production where Grunt output the files
css: './laravel/public/css/',
js: './laravel/public/js/'
},
// Task configuration
concat: {
//...
},
less: {
//...
},
uglify: {
//...
},
phpunit: {
//...
},
watch: {
//...
}
});
// Plugin loading
// Task definition
};
The stylesheets
Let's create the following filestructure:
/ laravel/public
/ assets
/ javascript
- backend.js
- frontend.js
/ stylesheets
- backend.less
- base.less
- fonts.less
- frontend.less
- variables.less
You can copy bootstrap.less from Bootstrap and rename it to /laravel/public/assets/stylesheets/base.less
You can copy variables.less from Bootstrap and put it to /laravel/public/assets/stylesheets/variables.less
The file /laravel/public/assets/stylesheets/frontend.less:
@import 'base.less';
//styles specific for the frontend
// ...
The file /laravel/public/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:
// Core variables and mixins
@import "variables.less";
@import "../vendor/bootstrap/less/mixins.less";
// Reset
@import "../vendor/bootstrap/less/normalize.less";
@import "../vendor/bootstrap/less/print.less";
// Core CSS
@import "../vendor/bootstrap/less/scaffolding.less";
@import "../vendor/bootstrap/less/type.less";
@import "../vendor/bootstrap/less/code.less";
@import "../vendor/bootstrap/less/grid.less";
@import "../vendor/bootstrap/less/tables.less";
@import "../vendor/bootstrap/less/forms.less";
@import "../vendor/bootstrap/less/buttons.less";
/*
...
...
*/
If you already copy the bootstrap.less to base.less, make sure you correct the path like the script above
And let's set up their tasks:
less: {
development: {
options: {
compress: false, //NOT minifying the result
},
files: {
//compiling frontend.less into frontend.css
"<%= paths.css %>frontend.css":"<%= paths.assets.css %>frontend.less",
//compiling backend.less into backend.css
"<%= paths.css %>backend.css":"<%= paths.assets.css %>backend.less"
}
},
production: {
options: {
compress: true, //minifying the result
},
files: {
//compiling frontend.less into frontend.min.css
"<%= paths.css %>frontend.min.css":"<%= paths.assets.css %>frontend.less",
//compiling backend.less into backend.min.css
"<%= paths.css %>backend.min.css":"<%= paths.assets.css %>backend.less"
}
}
},
Remember we already set path for
paths.css
andpaths.assets.css
. This way we can pass variables with<%= variable %>
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: [
'<%= paths.assets.vendor %>jquery/dist/jquery.js',
'<%= paths.assets.vendor %>bootstrap/dist/js/bootstrap.js',
'<%= paths.assets.js %>frontend.js'
],
dest: '<%= paths.js %>frontend.js',
},
js_backend: {
src: [
'<%= paths.assets.vendor %>jquery/dist/jquery.js',
'<%= paths.assets.vendor %>bootstrap/dist/js/bootstrap.js',
'<%= paths.assets.js %>backend.js'
],
dest: '<%= paths.js %>backend.js',
},
},
To run only this single task run
grunt concat
,grunt concat:js_frontend
orgrunt 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: {
'<%= paths.js %>frontend.min.js': '<%= paths.js %>frontend.js',
}
},
backend: {
files: {
'<%= paths.js %>backend.min.js': '<%= paths.js %>backend.js',
}
},
},
To run only this single task run
grunt uglify
, or sub taskgrunt uglify:frontend
orgrunt uglify:backend
in the command line.
The tests
Grunt can automatically run your tests in Laravel, but we need to make sure PhpUnit exist. laravel/vendor/bin/phpunit
.
Let’s update Laravel if you haven’t already. Head to laravel directory and perform this magic.
composer update
Open laravel/composer.json and check if
"phpunit/phpunit": "3.7.*"
are present inside"require": {}
.
If not, add manually and run
composer update
Let’s get back to Gruntfile.js and check this task definition:
phpunit: {
classes: {
},
options: {
}
},
Since this guide going to run grunt
outside laravel directory, phpunit
will not work properly. Usually it will check automatically for phpunit.xml.
Make sure you setup like below:
phpunit: {
classes: {
dir: 'laravel/app/tests/' //location of the tests
},
options: {
bin: 'laravel/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
'<%= paths.assets.vendor %>jquery/jquery.js',
'<%= paths.assets.vendor %>bootstrap/dist/js/bootstrap.js',
'<%= paths.assets.js %>frontend.js'
],
tasks: ['concat:js_frontend','uglify:frontend'], //tasks to run
options: {
livereload: true //reloads the browser
}
},
js_backend: {
files: [
//watched files
'<%= paths.assets.vendor %>jquery/jquery.js',
'<%= paths.assets.vendor %>bootstrap/dist/js/bootstrap.js',
'<%= paths.assets.js %>backend.js'
],
tasks: ['concat:js_backend','uglify:backend'], //tasks to run
options: {
livereload: true //reloads the browser
}
},
less: {
files: ['<%= paths.assets.css %>*.less'], //watched files
tasks: ['less'], //tasks to run
options: {
livereload: true //reloads the browser
}
},
tests: {
files: ['laravel/app/controllers/*.php','laravel/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
which, 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({
// paths
paths: {
assets: {
css: './laravel/public/assets/stylesheets/',
js: './laravel/public/assets/javascripts/',
vendor: './laravel/public/assets/vendor/'
},
css: './laravel/public/css/',
js: './laravel/public/js/'
},
// Task configuration
less: {
development: {
options: {
compress: false, //NOT minifying the result
},
files: {
//compiling frontend.less into frontend.css
"<%= paths.css %>frontend.css":"<%= paths.assets.css %>frontend.less",
//compiling backend.less into backend.css
"<%= paths.css %>backend.css":"<%= paths.assets.css %>backend.less"
}
},
production: {
options: {
compress: true, //minifying the result
},
files: {
//compiling frontend.less into frontend.css
"<%= paths.css %>frontend.min.css":"<%= paths.assets.css %>frontend.less",
//compiling backend.less into backend.css
"<%= paths.css %>backend.min.css":"<%= paths.assets.css %>backend.less"
}
}
},
concat: {
options: {
separator: ';',
},
js_frontend: {
src: [
'<%= paths.assets.vendor %>jquery/dist/jquery.js',
'<%= paths.assets.vendor %>bootstrap/dist/js/bootstrap.js',
'<%= paths.assets.js %>frontend.js'
],
dest: '<%= paths.js %>frontend.js',
},
js_backend: {
src: [
'<%= paths.assets.vendor %>jquery/dist/jquery.js',
'<%= paths.assets.vendor %>bootstrap/dist/js/bootstrap.js',
'<%= paths.assets.js %>backend.js'
],
dest: '<%= paths.js %>backend.js',
},
},
uglify: {
options: {
mangle: false // Use if you want the names of your functions and variables unchanged
},
frontend: {
files: {
'<%= paths.js %>frontend.min.js': '<%= paths.js %>frontend.js',
}
},
backend: {
files: {
'<%= paths.js %>backend.min.js': '<%= paths.js %>backend.js',
}
},
},
phpunit: {
classes: {
dir: 'laravel/app/tests/' //location of the tests
},
options: {
bin: 'laravel/vendor/bin/phpunit',
colors: true
}
},
watch: {
js_frontend: {
files: [
//watched files
'<%= paths.assets.vendor %>jquery/dist/jquery.js',
'<%= paths.assets.vendor %>bootstrap/dist/js/bootstrap.js',
'<%= paths.assets.js %>frontend.js'
],
tasks: ['concat:js_frontend','uglify:frontend'], //tasks to run
options: {
livereload: true //reloads the browser
}
},
js_backend: {
files: [
//watched files
'<%= paths.assets.vendor %>jquery/dist/jquery.js',
'<%= paths.assets.vendor %>bootstrap/dist/js/bootstrap.js',
'<%= paths.assets.js %>backend.js'
],
tasks: ['concat:js_backend','uglify:backend'], //tasks to run
options: {
livereload: true //reloads the browser
}
},
less: {
files: ['<%= paths.assets.css %>*.less'], //watched files
tasks: ['less'], //tasks to run
options: {
livereload: true //reloads the browser
}
},
tests: {
files: ['laravel/app/controllers/*.php','laravel/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 with Laravel installation is available on
Github with extra Bower dependencies.
If you'd like to contribute, feel free to issue a pull request.
Beer me!
Thank you!