JavaScript
Article

A Simple Gulp’y Workflow For Sass

By Hugo Giraudel

I have recently been in charge of optimizing the Sass side of quite a big Rails project, and one of most important things to do was to improve the compilation time. Because of the Sass architecture in place and the fact that Ruby Sass (through the Rails asset pipeline in this case) tends to be slow when dealing with a huge number of files, it could take up to 40 seconds to compile the stylesheets. Talk about a fast development process. :)

My idea was to move away from the asset pipeline and embrace the speed of LibSass. To make things easier I decided to go with a simple Gulp workflow. It was the first time I would be using Gulp, and I must say it was quite an enjoyable experience (which was not the case for Grunt as far as I am concerned).

In this short article, let’s just have a quick tour on how to set up a Gulp’y workflow to work with Sass. Here is what we will include:

  • Unsurprisingly, Sass compilation with LibSass
  • Generating sourcemaps for easier debugging
  • Prefixing CSS with Autoprefixer
  • Generating Sass documentation with SassDoc

Compiling Sass

The first thing to do is to install the dependencies and to create a Gulpfile.js. We will need Gulp (no shit, Sherlock), but also gulp-sass to compile our stylesheets:

$ npm install gulp gulp-sass --save-dev

This line tells npm to install both gulp and gulp-sass packages as development dependencies. You can now find them in the devDependencies object of your package.json. And the Gulpfile.js:

var gulp = require('gulp');
var sass = require('gulp-sass');

Wow, that was short. What we need now is a task to run Sass (actually gulp-sass) on our stylesheets folder.

var input = './stylesheets/**/*.scss';
var output = './public/css';

gulp.task('sass', function () {
  return gulp
    // Find all `.scss` files from the `stylesheets/` folder
    .src(input)
    // Run Sass on those files
    .pipe(sass())
    // Write the resulting CSS in the output folder
    .pipe(gulp.dest(output));
});

That’s it! We can now compile our stylesheets using LibSass thanks to a very minimal Gulp task. What about that? We can pass options to gulp-sass to compile stylesheets in expanded mode and to print errors in console:

var sassOptions = {
  errLogToConsole: true,
  outputStyle: 'expanded'
};

gulp.task('sass', function () {
  return gulp
    .src(input)
    .pipe(sass(sassOptions).on('error', sass.logError))
    .pipe(gulp.dest(output));
});

Adding sourcemaps

So far, so good. Now, what about generating sourcemaps? In case you don’t know what sourcemaps are, it basically is a way to map compressed production sources with expanded development sources in order to make debugging live code easier. They are not restricted to CSS at all, sourcemaps can be used in JavaScript as well.

We have a nice article about sourcemaps here at SitePoint. Feel free to give it a read before going on if you feel a bit short on the understanding of sourcemaps.

Okay, so to add sourcemaps generation to our task, we need to install gulp-sourcemaps:

$ npm install gulp-sourcemaps --save-dev

And now let’s optimise our task:

var gulp = require('gulp');
var sass = require('gulp-sass');
var sourcemaps = require('gulp-sourcemaps');

// ... variables

gulp.task('sass', function () {
  return gulp
    .src(input)
    .pipe(sourcemaps.init())
    .pipe(sass(sassOptions).on('error', sass.logError))
    .pipe(sourcemaps.write())
    .pipe(gulp.dest(output));
});

By default, gulp-sourcemaps writes the sourcemaps inline in the compiled CSS files. Depending on the project setup, we might want to write them in separate files, in which case we can specify a path relative to the gulp.dest() destination in the sourcemaps.write()function like:

gulp.task('sass', function () {
  return gulp
    .src(input)
    .pipe(sourcemaps.init())
    .pipe(sass(sassOptions).on('error', sass.logError))
    .pipe(sourcemaps.write('./stylesheets/maps'))
    .pipe(gulp.dest(output));
});

Bringing Autoprefixer to the party

I won’t go into much detail about why using Autoprefixer is better than writing vendor by hand (or with a mixin which is basically the same thing), but roughly Autoprefixer is a post-processing step meaning it actually updates already compiled stylesheets to add relevant prefixes based on an up-to-date database and a given configuration. In other words, you tell Autoprefixer which browsers you want to support, and it adds only relevant prefixes to the stylesheets. Zero effort, perfect support (please remind me to patent this catch phrase).

To include Autoprefixer in our Gulp’y workflow, we only need it to pipe it after Sass has done its thing. Then Autoprefixer updates the stylesheets to add prefixes.

First, let’s install it (you get the gist by now):

$ npm install gulp-autoprefixer --save-dev

Then we add it to our task:

var gulp = require('gulp');
var sass = require('gulp-sass');
var sourcemaps = require('gulp-sourcemaps');
var autoprefixer = require('gulp-autoprefixer');

// ... variables

gulp.task('sass', function () {
  return gulp
    .src(input)
    .pipe(sourcemaps.init())
    .pipe(sass(sassOptions).on('error', sass.logError))
    .pipe(sourcemaps.write())
    .pipe(autoprefixer())
    .pipe(gulp.dest(output));
});

Right now, we run with the default configuration from Autoprefixer which is

  • Browsers with over 1% market share,
  • Last 2 versions of all browsers,
  • Firefox ESR,
  • Opera 12.1

We can use our own configuration like so:

var gulp = require('gulp');
var sass = require('gulp-sass');
var sourcemaps = require('gulp-sourcemaps');
var autoprefixer = require('gulp-autoprefixer');

// ... variables
var autoprefixerOptions = {
  browsers: ['last 2 versions', '> 5%', 'Firefox ESR']
};

gulp.task('sass', function () {
  return gulp
    .src(input)
    .pipe(sourcemaps.init())
    .pipe(sass(sassOptions).on('error', sass.logError))
    .pipe(sourcemaps.write())
    .pipe(autoprefixer(autoprefixerOptions))
    .pipe(gulp.dest(output));
});

Release the docs!

The last, but not least, tool to add to our workflow, Sass documentation generation with SassDoc. SassDoc is to Sass what JSDoc is to JavaScript: a documentation tool. It parses your stylesheets looking for comment blocks documenting variables, mixins, functions and placeholders.

If your project uses SassDoc (it should!), you can add the automatic documentation generation in your Gulp workflow.

The cool thing with SassDoc is that it can be piped directly in Gulp because its API is Gulp compatible. So you don’t actually have a gulp-sassdoc plugin.

npm install sassdoc --save-dev
var gulp = require('gulp');
var sass = require('gulp-sass');
var sourcemaps = require('gulp-sourcemaps');
var autoprefixer = require('gulp-autoprefixer');
var sassdoc = require('sassdoc');

// ... variables

gulp.task('sass', function () {
  return gulp
    .src(input)
    .pipe(sourcemaps.init())
    .pipe(sass(sassOptions).on('error', sass.logError))
    .pipe(sourcemaps.write())
    .pipe(autoprefixer(autoprefixerOptions))
    .pipe(gulp.dest(output))
    .pipe(sassdoc())
    // Release the pressure back and trigger flowing mode (drain)
    // See: http://sassdoc.com/gulp/#drain-event
    .resume();
});

Note that depending on the size of your project and the number of documented items, SassDoc can take up to a few of seconds to run (rarely above 3 as far as I’ve noticed), so you might want to have a separate task for this.

gulp.task('sassdoc', function () {
  return gulp
    .src(input)
    .pipe(sassdoc())
    .resume();
});

Again, we use the default configuration but we can use our own if we want to.

var sassdocOptions = {
  dest: './public/sassdoc'
};

gulp.task('sassdoc', function () {
  return gulp
    .src(input)
    .pipe(sassdoc(sassdocOptions))
    .resume();
});

I’m watching you

There is still something we can do before leaving: creating a watch task. The point of this task would be to watch for changes in stylesheets to recompile them again. It is very convenient when working on the Sass side of the project so you don’t have to run the sass task by hand every time you save a file.

gulp.task('watch', function() {
  return gulp
    // Watch the input folder for change,
    // and run `sass` task when something happens
    .watch(input, ['sass'])
    // When there is a change,
    // log a message in the console
    .on('change', function(event) {
      console.log('File ' + event.path + ' was ' + event.type + ', running tasks...');
    });
});

Here is another reason why I recommend not including SassDoc in the sass task: you probably don’t want to regenerate the docs every time you touch a stylesheet. This is likely something you want to do on build or push, maybe with a pre-commit hook.

Adding the final touch

A last, yet important, thing to think about: running sass in the default task.

gulp.task('default', ['sass', 'watch' /*, possible other tasks... */]);

The array passed as second argument of the task(..) function is a list of dependency tasks. Basically, it tells Gulp to run those tasks before running the one specified as a third argument (if any).

Also, we could probably create a prod task that could be run right before deploying to production (maybe with a git hook). This task should:

  • Compile Sass in compressed mode
  • Prefix CSS with Autoprefixer
  • Regenerate SassDoc documentation
  • Avoid any sourcemaps
gulp.task('prod', ['sassdoc'], function () {
  return gulp
    .src(input)
    .pipe(sass({ outputStyle: 'compressed' }))
    .pipe(autoprefixer(autoprefixerOptions))
    .pipe(gulp.dest(output));
});

Final thoughts

That’s it folks! In just a couple of minutes and a few lines of JavaScript, we have managed to create a powerful little Gulp workflow. You can find the full file here. What would you add to it?

  • Arden de Raaij

    Nice, looks quite like my workflow! I guess the only thing I’ve added is an error handler so the watch doesn’t break on error, and the gulp notification plugin to get some notifications when the style task is done.

    • http://hugogiraudel.com/ Hugo Giraudel

      Care sharing your Gulpfile maybe? :)

      • Arden de Raaij

        Of course! This is the gulpfile for a static site generator thingy I made: https://github.com/aderaaij/totallystatical/blob/master/gulpfile.js

        It’s pretty well commented, does not include any docs though, I might add this thanks to your blogpost!

      • http://macr.ae/ Callum Macrae

        Check out gulp-plumber for error handling in Gulp 3. Gulp 4 has error handling built in (–continue or something like that; it’s completely undocumented right now though)

        • Arden de Raaij

          Plumber is what I use indeed. Would be nice to do that without a plugin though! I should have a look into Gulp 4 in that case!

          As for my Gulpfile, you can find it right here: https://github.com/aderaaij/totallystatical/blob/master/gulpfile.js

          It does a couple of other things (basically a static site generator with Jade as template language) but it’s pretty well commented.

      • Arden de Raaij

        tried, but my comment seems to be removed?

      • http://studiorgb.uk Paweł Grzybek

        https://github.com/studiorgb/Starter/blob/master/gulpfile.js

        Holla hugo. Here you have my example how to handle errors without breaking watch process. Nice article by the way – as always!

  • Gluten

    Nice article. :)
    “What would you add to it?” Maybe `gulp-notify` and `gulp-livereload`? If you can post a gist on how to handle the new nodesass error system with notify that would be very useful as I I’ve capitulated after a few tries. :D

    P.S: your gist link is broken.

    • http://hugogiraudel.com/ Hugo Giraudel

      Fixed!

    • http://www.alwaystwisted.com/ Stuart Robson

      Hey @disqus_zr5zTAp2bd:disqus, good catch. Fixed with an updated gist from Hugo.

    • http://studiorgb.uk Paweł Grzybek

      Livereload is old school. We have a new kid on the block, BrowserSync :)

      • Alex McCabe

        I’d love to use BrowserSync, but it doesn’t play too nicely with Vagrant. If anyone can get it working and not slow as hell, let me know how you did it!

        • http://www.facebook.com/profile.php?id=61401254 Kevin Coyle

          Are you running your task on the host or the guest? I’d run it on the host and use a proxy (see the browsersync docs for help on the proxies)

          • Alex McCabe

            Running on the host through a proxy is exactly what I have done, but it was so slow I had to go back to LiveReload. We do have an older version of both Vagrant and the box that is running in it, and for time reasons we can’t update at the moment. I have no idea if that would be part of the problem.

          • http://chuquangtu.com/blog Chu Quang Tu

            I ran on Guest. It doesn’t make sense if I have to install whole bunch of “tech” stuff to host If I use Vagrant

        • http://chuquangtu.com/blog Chu Quang Tu

          After 1 year, it still doesn’t play well with Vagrant. Horribly slow T_T

  • http://macr.ae/ Callum Macrae

    gulp-autoprefixer supports gulp-sourcemaps; is there any reason it’s outside the sourcemaps block?

  • Matthew Felgate

    Nice article!

    I include and recommend these:

    * gulp-minify-css to minify the CSS (more than Sass compression output)

    * similarly gulp-uglify for minifying javascript

    * Image optimisation using gulp-imagemin or gulp-image-optimization

    * gulp-livereload or browser-sync for seeing live changes in the browser

    * gulp-uncss is great for checking website and removing unused css

    * generating critical CSS for the homepage using critical or critical css, and gulp-css-url-adjuster

  • http://www.kaelig.fr/ Kaelig

    Thanks, very simple setup and clear steps!

    For the production task, I would recommend to keep the normal Sass development output and pipe it through clean-css (https://github.com/jakubpawlowicz/clean-css) that will optimize the CSS. It also avoids running into bugs that exist in the Sass compressed mode.

  • Admire The Web

    Great tutorial.

    Defintely needs some error notifications.

    I added ‘gulp-notify’.

    var notify = require(“gulp-notify”);

    .pipe(sass(sassOptions)).on(‘error’, notify.onError(function (error) {
    return “Problem file : ” + error.message;
    }))

    • http://hugogiraudel.com/ Hugo Giraudel

      Good idea. :)

  • http://hugogiraudel.com/ Hugo Giraudel

    Good hint Kaelig. I also heard that clean-css does a better job than Sass at optimising CSS.

  • http://hugogiraudel.com/ Hugo Giraudel

    You can use Plumber apparently. Check other comments below. ;)

    • http://studiorgb.uk Paweł Grzybek

      Thats true I used plumber before but I do not see the point now. You can handle errors without plugins and without breaking watching. This just works for me. I always try to keep things simple. “Keep it simple stupid ” yeah? :)

  • Crispen Smith

    I’ve been avoiding gulp and grunt for a while now because I currently use a ruby solution that compiles my Sass and I don’t have any other build tasks (I rely heavily on server side partial templates so it’s tricky to lint consolidated pages).
    BUT… this article actually got me excited about Gulp, I’m now going to dig in, thank you Hugo.

  • john

    Hugo, i’m struggling with how to add his sass Plugin:
    https://github.com/madastro/sass-timestamp
    Where would you put a ‘require “sass-timestamp”‘?
    Thanks!

  • isaacalves

    Great workflow! But there’s an issue with sourcemaps:

    If I have bootstrap.scss imported into my main scss file, I get the following error during `sourcemaps.write()`:

    Error: “/bower_components/bootstrap-sass/assets/stylesheets/_bootstrap.scss” is not in the SourceMap.

    What’s wrong?

  • http://www.reonysstudio.com/ Reony

    I had to put .pipe(sourcemaps.write()) after .pipe(autoprefixer()) to get it to go without errors. There a better way? Someone mentioned below that autoprefixer supports sourcemaps?

  • J. Kurian

    I also had to put sourcemaps.write() after autoprefixer(). Are there any updates from gulp-sass? The issue link is a 404. :(

    • Glen

      I fixed the link (remove the closing parenthesis at the end).

  • http://chuquangtu.com/blog Chu Quang Tu

    WOW. why don’t I know it sooner ;(
    @Hugo Giraduel the link to `full file here.` is not found. Could you please update it ?

  • mefynn

    you moved your files!!!!!
    why
    please provide the new link

  • Ark Roy

    @font-face doesn’t work on gulp-sass

  • http://tomasio.at/ tomasio reicht

    “You can now find them in the devDependencies object of your package.json. And the Gulpfile.js:”
    Question 1: Where should my package.json be located, cannot decide which is the right one?
    Question 2: I found also many files named Gulpfile.js. Which one is it, and do you mean I should add the lines

    var gulp = require(‘gulp’);
    var sass = require(‘gulp-sass’);

    As I find first line included in each of my gulpfile.js, but not the second. Please enlighten me ;)

    • http://hugogiraudel.com/ Hugo Giraudel

      Your package.json and your Gulpfile.js should usually be at the root of your project directory. You have to create your own, not use any existing one from one of your dependencies.

      • http://tomasio.at/ tomasio reicht

        Ty for your quick answer, Hugo. As I am new to this: Where is _my_ project directory, as I did not create any project yet? I just wanted to use autoprefixer instead compass. So do I have to create sth. like my-project-folder/package.json and my-project-folder/Gulpfile.js? To create a package.json vom scratch is a science per se :(

        • http://hugogiraudel.com/ Hugo Giraudel

          Yep. In your project folder (the one with stylesheets using Compass right now), you can create a package.json and a Gulpfile.js. There are quite a few tutorials on the web to do that from scratch. :)

  • j7an

    Autoprefixer will error out if you specify a destination path for the sourcemaps. To fix you must move autoprefixer above sourcemaps.write

  • Moussa

    Thanks Hugo Giraudel, brilliant and clear manual!!

Recommended

Learn Coding Online
Learn Web Development

Start learning web development and design for free with SitePoint Premium!

Get the latest in JavaScript, once a week, for free.