LESS and SCSS are great tools for writing CSS. Now that I am starting to incorporate these tools at my day job, I am left with a Continuous Integration (CI) issue. The problem I have is that I prefer not to commit compiled code into my repositories. So I am left with trying to develop a build process that works on my team’s local environments as well as our CI stack (Development, Quality Assurance and Production). After about a half day of research I concluded that Grunt was the tool that I will use. Grunt is a JavaScript tool used for executing tasks. It installs using Node Package Manager, and it has its own packages that you can individually install for each project. Below I will demonstrate the steps I took to write Grunt tasks that compile LESS/SCSS files and minify CSS/JS files.

I want to thank Ryan Christiani for his tutorial Getting started with Grunt and Sass. It gave me a good starting spot for building these Grunt tasks.

Dependencies

First things first. You will need to have Ruby installed to compile SCSS files and NodeJs to install Grunt. If you need a tutorial on how to install these applications, you can refer to Ryan Christiani’s blog post above.

Now that you have Ruby and NodeJs installed you can install Grunt CLI globally using the command below.

npm install -g grunt-cli

Now lets go to your project directory. We are going to create a couple files within your project that will be used for installing Grunt packages and executing the tasks. Create a package.json file with the text shown below. Feel free to change the project name and version number. This file is used by Node Package Manager to store project information, including packages.

{
"name": "Project Name",
"version": "0.0.1",
"devDependencies": {
// installed packages will go here
}
}

Lets go ahead and install the packages we need for our project using the code below. The --save-dev option is used install the packages name and version under devDependencies in your package.json file. If you have worked with SCSS/LESS and minification, then you should understand what most of these packages are for. The ones you might not be familiar with is Watch and Clean. Watch is used to listen for file changes and then executing tasks based on those files that have changed. Clean is used for deleting files.

npm install grunt --save-dev
npm install grunt-contrib-sass --save-dev
npm install grunt-contrib-watch --save-dev
npm install grunt-contrib-cssmin --save-dev
npm install grunt-contrib-clean --save-dev
npm install grunt-contrib-less --save-dev
npm install grunt-contrib-uglify --save-dev

Your package.json file show now looks something like this.

{
"name": "Project Name",
"version": "0.0.1",
"devDependencies": {
"grunt": "~0.4.1",
"grunt-contrib-sass": "~0.5.0",
"grunt-contrib-watch": "~0.5.3",
"grunt-contrib-cssmin": "~0.6.2",
"grunt-contrib-clean": "~0.5.0",
"grunt-contrib-less": "~0.7.0",
"grunt-contrib-uglify": "~0.2.4"
}
}

Grunt Tasks

Lets go over all the tasks we need Grunt to accomplish.

  1. Watch for file changes (local development only)
  2. Delete old files
  3. Compile SCSS files
  4. Compile LESS files
  5. Minify CSS files
  6. Minify JS files
  7. Delete none minified CSS files (CI stack only)

The code did not formatted correctly in this post so here is a copy of the Gruntfile.js if you would like to download it. The first two lines of code is the starting point for Grunt. It will be the same for any Grunt file you create. The third line (pkg: grunt.file.readJSON('package.json'),) will ensure that all the packages are installed before executing the tasks. The rest of the initConfig are all the configurations for the different Grunt task modules and relative paths. At the end of the file you will see grunt.loadNpmTasks('<package name>');. This loads the packages that we installed so they can be used. The few lines of code below that (grunt.registerTask('<task name>', ['<task>', '<task>', ...]);) are the Grunt tasks that can be executed from the terminal (ex. grunt buildAll). The default registered task can be executed by simply typing in grunt and hitting enter. Looking at the default registered task you will see that it is watching for changes of the SCSS, LESS, and JavaScript files. This is setup to be used for the developer’s local builds. When a SCSS or LESS file is changed, it will delete all the CSS files, compile SCSS and LESS and place them in the static/css/ directory, and minify them. When a JavaScript file has changed, all the minified JS files are deleted and then re-minified. I created one registered Grunt task that is used for CI deployment, and it is called buildAll. This task closely resembles the default task except that it will remove the non-minified CSS files after the SCSS/LESS files have been compiled and minified.

*One thing to note about the SCSS and LESS files is the file names can not be the sameĀ (not including the extension). If the you have a SCSS file that has the same name as a LESS file then the CSS file will be overwritten by the last compilation task, which in this case would be the LESS compilation task.


module.exports = function(grunt) {
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
paths: {
sass: 'static/sass/',
less: 'static/less/',
css: 'static/css/',
js: 'static/js/'
},
clean: {
cssAll: {
expand: true,
cwd: '<%= paths.css %>',
src: ['**/*.css']
},
css: {
expand: true,
cwd: '<%= paths.css %>',
src: ['**/*.css', '!**/*.min.css']
},
jsMin: {
expand: true,
cwd: '<%= paths.js %>',
src: ['**/*.min.js']
}
},
less: {
sources: {
files: [{
expand: true,
cwd: '<%= paths.less %>',
src: ['**/*.less'],
dest: '<%= paths.css %>',
ext: '.css'
}]
}
},
sass: {
dist: {
files: [{
expand: true,
cwd: '<%= paths.sass %>',
src: ['**/*.scss'],
dest: '<%= paths.css %>',
ext: '.css'
}]
}
},
uglify: {
options: {
mangle: false
},
dist: {
files: [{
expand: true,
cwd: '<%= paths.js %>',
src: ['**/*.js', '!<%= paths.js %>**/*.min.js'],
dest: '<%= paths.js %>',
ext: '.min.js'
}]
}
},
cssmin: {
minify: {
expand: true,
cwd: '<%= paths.css %>',
src: ['**/*.css', '!**/*.min.css'],
dest: '<%= paths.css %>',
ext: '.min.css'
}
},
watch: {
css: {
files: ['<%= paths.sass %>**/*.scss', '<%= paths.less %>**/*.less'],
tasks: ['clean:cssAll', 'sass', 'less', 'cssmin']
},
js: {
files: ['<%= paths.js %>**/*.js', '!<%= paths.js %>**/*.min.js'],
tasks: ['clean:jsMin', 'uglify']
}
}
});
grunt.loadNpmTasks('grunt-contrib-clean');
grunt.loadNpmTasks('grunt-contrib-sass');
grunt.loadNpmTasks('grunt-contrib-cssmin');
grunt.loadNpmTasks('grunt-contrib-less');
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-contrib-watch');
grunt.registerTask('default', ['watch']);
grunt.registerTask('cleanAll', ['clean']);
grunt.registerTask('cleanCss', ['clean:css']);
grunt.registerTask('cleanJsMin', ['clean:jsMin']);
grunt.registerTask('buildAll', ['clean', 'sass', 'less', 'cssmin', 'clean:css', 'uglify']);
}