Synchronous Streams with Gulp

I’ve been a huge Grunt fan for awhile now, and it wasn’t until recently that I started using an up and coming tool Gulp for my tasks.

Special thanks to Dan Tello over at Viget for his post that really inspired me to get into this.

With Gulp, it’s all about the stream, man. You use asyncronous node pipe methods to chain streams together. It’s really tricky to understand if you’re not familiar with this concept, and it took me awhile (I’m still only now just getting it as I type).

With Grunt, it’s all about configuration; with Gulp, it’s all about building your tasks dynamically. It only took me an hour or two to migrate an existing Gruntfile over to a gulpfile. Including a learning curve with a new framework, this isn’t too bad.

I won’t dive too deep into it today, but one problem I ran into was with actually getting the tasks to finish before I started another one. It seemed like a bug, until I realized what was happening:

Gulp was working so fast, one stream hadn’t finished in time for the other to occur.

Example:

A common use case for this is compiling CSS, then minifying it. Seems simple, right? With Grunt, you’d just stack the tasks together, and since Grunt runs them one at a time, you have no issues.

Gruntfile.js
1
grunt.registerTask('default', ["coffee", "stylus", "jst", "concurrent:compress", "watch"]);

With Gulp, however, it’s not quite as simple.

Just an FYI here, I’m following Dan’s model of modularizing my tasks into individual files, hence the alternate filenames in the task definitions below. If you’re going with a basic setup, these can all live in your gulpfile.

gulpfile.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
gulp.task("compile", [
  "coffee",
  "stylus"
]);

gulp.task("compress", [
  "concat_app",
  "concat_vendor",
  "uglify_vendor",
  "cssmin"
]);

gulp.task("default", [
  "compile",
  "compress",
  "watch"
]);

gulp.task("build", [
  "compile",
  "compress"
]);

This doesn’t work as you expect. If you remove all CSS files from your project and run gulp build, you’ll end up with a compiled CSS file, but no minified version. However, if you run gulp build again, you’ll get your minified version, because the CSS file is already there.

Fortunately, gulp ships with an orchestrator that allows you to essentially specify dependencies that other tasks require. These dependencies are other Gulp task definitions that return a stream.

To specify one, just add an array after you task name, as with my cssmin task below, where I’ve added ['stylus'].

Cssmin task:
cssmin.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var cssmin = require('gulp-cssmin'),
  rename = require('gulp-rename');

module.exports = function() {
  return gulp.task("cssmin", ['stylus'], function() {
    gulp.src("public/stylesheets/vendor.css")
      .pipe(cssmin())
      .pipe(rename("vendor.min.css"))
      .pipe(gulp.dest("public/stylesheets"));

    gulp.src("public/stylesheets/application.css")
      .pipe(cssmin())
      .pipe(rename("application.min.css"))
      .pipe(gulp.dest("public/stylesheets"));
  });

}
Stylus task:
stylus.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
var gulp = require("gulp"),
  accord = require("gulp-accord"),
  cssmin = require("gulp-minify-css"),
  rename = require("gulp-rename"),
  notify = require("gulp-notify"),
  livereload = require("gulp-livereload"),
  stylus = require('gulp-stylus');

module.exports = function() {
  gulp.task("stylus", function() {
    gulp.src("public/stylesheets/styl/application.styl")
      .pipe(stylus({
        set: [
          "resolve url",
          "include-css",
          "linenos",
          "compress"
        ]
      })).on("error", notify.onError({
        message: "<%= error.message %>",
        title: "Stylus Error"
      }))
      .pipe(gulp.dest("public/stylesheets"))
      .pipe(livereload());

    return gulp.src("public/stylesheets/styl/vendor.styl")
      .pipe(stylus({
        set: [
          "include css",
          "linenos"
        ]
      })).on("error", notify.onError({
        message: "<%= error.message %>",
        title: "Stylus Error"
      }))
      .pipe(gulp.dest("public/stylesheets"));
  });
};

Now for the tricky part. See on line 33 where I’m returning the task itself? If you omit this line, the dependency is considered resolved and will never run. This line is super important and was confusing the hell out of me for quite awhile there, so I thought I’d share in case someone else ends up confused.

Ideally, I could split this task into two separate task definitions, but for this project that was good enough. I always want my vendor to be compiled, and though it rarely changes, with Gulp the compilation was so fast it was not even noticeable without it.

That’s all for today: make sure you return that stream!