Getting Started with Gulp

I’ve used a variety of build tools through my career, from make to ant to maven/maven2 to gradle, through rake and now grunt and gulp. People tend to be zealots about their particular tool of choice. I’m a lot less fussed. I value simplicity over almost everything else. I don’t value typing less at all - if a framework makes me type 30 more characters but it’s easier to understand what’s going on then it wins for me. I also value ability to do what I need now (or know I’ll need in the future) over what I may at some point in a distant future possibly value and want.

Development “Requirements”

I want my build system to do just a few things, but to do them well and simply.

  • I want the system to manage dependencies - track them, download them and inject them. For front-end development this effectively means I want it to play nicely with bower
  • The system needs to support live reloading. If I save a file, I don’t want to have to go through a full stop-compile-restart cycle every time.
  • The system should process intermediate files - so convert from less/stylus/sass as needed
  • While developing, run basic a lint tool like jshint to help me not screw up

There are additional tasks I’ll need to handle testing and deployment but those can come later.

Install Gulp

If you don’t have Gulp installed globally, install using npm.

1
mbp:~/dev/cwrky$ sudo npm install -g gulp

Run npm init to create local package.json file:

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
39
40
41
42
43
44
mbp:~/dev/cwrky$ npm init
This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sane defaults.

See `npm help json` for definitive documentation on these fields
and exactly what they do.

Use `npm install <pkg> --save` afterwards to install a package and
save it as a dependency in the package.json file.

Press ^C at any time to quit.
name: (cwrky)
version: (1.0.0)
description:
entry point: (index.js)
test command:
git repository: (https://github.com/akshayrangnekar/cwrky.git)
keywords:
author:
license: (ISC)
About to write to /Users/akshay/Dev/cwrky/package.json:

{
"name": "cwrky",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "https://github.com/akshayrangnekar/cwrky.git"
},
"author": "",
"license": "ISC",
"bugs": {
"url": "https://github.com/akshayrangnekar/cwrky/issues"
},
"homepage": "https://github.com/akshayrangnekar/cwrky"
}


Is this ok? (yes) yes

Install gulp locally

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
mbp:~/dev/cwrky$ npm install --save-dev gulp
npm WARN package.json cwrky@1.0.0 No description
npm WARN package.json cwrky@1.0.0 No README data
gulp@3.8.11 node_modules/gulp
├── pretty-hrtime@0.2.2
├── interpret@0.3.10
├── deprecated@0.0.1
├── archy@1.0.0
├── minimist@1.1.1
├── semver@4.3.3
├── tildify@1.0.0 (user-home@1.1.1)
├── v8flags@2.0.5 (user-home@1.1.1)
├── orchestrator@0.3.7 (stream-consume@0.1.0, sequencify@0.0.7, end-of-stream@0.1.5)
├── chalk@0.5.1 (ansi-styles@1.1.0, escape-string-regexp@1.0.3, supports-color@0.2.0, has-ansi@0.1.0, strip-ansi@0.3.0)
├── gulp-util@3.0.4 (array-differ@1.0.0, beeper@1.0.0, object-assign@2.0.0, array-uniq@1.0.2, lodash._reescape@3.0.0, lodash._reevaluate@3.0.0, lodash._reinterpolate@3.0.0, replace-ext@0.0.1, vinyl@0.4.6, chalk@1.0.0, lodash.template@3.5.0, through2@0.6.5, multipipe@0.1.2, dateformat@1.0.11)
├── liftoff@2.0.3 (extend@2.0.0, flagged-respawn@0.3.1, resolve@1.1.6, findup-sync@0.2.1)
└── vinyl-fs@0.3.13 (graceful-fs@3.0.6, mkdirp@0.5.0, defaults@1.0.2, vinyl@0.4.6, strip-bom@1.0.0, through2@0.6.5, glob-stream@3.1.18, glob-watcher@0.0.6)
mbp:~/dev/cwrky$

Prepare directory structure

Create initial directory structure

1
2
3
4
mbp:~/dev/cwrky$ mkdir -p src/scripts
mbp:~/dev/cwrky$ mkdir -p src/styl
mbp:~/dev/cwrky$ mkdir -p src/html
mbp:~/dev/cwrky$ mkdir -p src/vendor

I keep each component separated while developing - this makes it easier to open specific folders in the best appropriate tool without them thinking that they should handle others. So for example I can easily use Coda for HTML, Sublime Text for Javascripts and TextMate for Stylus if I want. I also create a vendor directory - this will include HTML, CSS and Javascript. The reason for this is that I often use templates as a starting point. I want to keep them isolated, and in a place I don’t touch those files at all. It makes it easy to upgrade them in the future.

Wire up stylus

Create a very basic Stylus file like this, in src/styl

Install gulp-stylus

1
2
3
4
5
6
7
8
9
10
11
mbp:~/dev/cwrky$ npm install --save-dev gulp-stylus
npm WARN package.json cwrky@1.0.0 No description
npm WARN package.json cwrky@1.0.0 No README data
gulp-stylus@2.0.1 node_modules/gulp-stylus
├── replace-ext@0.0.1
├── vinyl-sourcemaps-apply@0.1.4 (source-map@0.1.43)
├── through2@0.6.5 (xtend@4.0.0, readable-stream@1.0.33)
├── gulp-util@3.0.4 (array-differ@1.0.0, beeper@1.0.0, object-assign@2.0.0, array-uniq@1.0.2, lodash._reescape@3.0.0, lodash._reinterpolate@3.0.0, lodash._reevaluate@3.0.0, minimist@1.1.1, vinyl@0.4.6, chalk@1.0.0, lodash.template@3.5.0, dateformat@1.0.11, multipipe@0.1.2)
├── stylus@0.50.0 (css-parse@1.7.0, mkdirp@0.3.5, debug@2.1.3, source-map@0.1.43, glob@3.2.11, sax@0.5.8)
├── accord@0.15.2 (indx@0.2.3, convert-source-map@0.4.1, fobject@0.0.3, when@3.7.2, resolve@1.1.6, glob@4.5.3, uglify-js@2.4.20)
└── lodash@3.7.0

Define a very basic styl task in gulpfile.js

gulpfile.jsview raw
1
2
3
4
5
6
7
8
9
// Include gulp
var gulp = require('gulp');
var stylus = require('gulp-stylus');

gulp.task('styl', function() {
return gulp.src('src/styl/main.styl')
.pipe(stylus())
.pipe(gulp.dest('app/css'))
});

To combine multiple inputs, install and wire up gulp-concat

1
2
3
4
5
6
7
mbp:~/dev/cwrky$ npm install --save-dev gulp-concat
npm WARN package.json cwrky@1.0.0 No description
npm WARN package.json cwrky@1.0.0 No README data
gulp-concat@2.5.2 node_modules/gulp-concat
├── through2@0.6.5 (xtend@4.0.0, readable-stream@1.0.33)
├── gulp-util@3.0.4 (array-differ@1.0.0, beeper@1.0.0, object-assign@2.0.0, array-uniq@1.0.2, lodash._reinterpolate@3.0.0, lodash._reescape@3.0.0, lodash._reevaluate@3.0.0, replace-ext@0.0.1, minimist@1.1.1, vinyl@0.4.6, chalk@1.0.0, lodash.template@3.5.0, multipipe@0.1.2, dateformat@1.0.11)
└── concat-with-sourcemaps@1.0.2 (source-map@0.4.2)

In the gulpfile:

1
2
3
4
5
6
7
8
var concat = require('gulp-concat');

gulp.task('styls', function() {
return gulp.src('src/styl/**/*.styl')
.pipe(stylus())
.pipe(concat('combined.css'))
.pipe(gulp.dest('app/css'))
});

Clean and HTML

We need some way to clean our build. Install vanilla node del using npm install --save-dev del, then wire up a clean task. For simplicity, added a simple task to copy html over, and a default task to run both html and styl.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var del = require('del');

var config = {
appPath: './app',
bowerDir: './bower_components'
}

gulp.task('styl', function() {
return gulp.src('src/styl/main.styl')
.pipe(stylus())
.pipe(gulp.dest(config.appPath + '/css'))
});

gulp.task('html', function() {
return gulp.src('src/html/**/*.html')
.pipe(gulp.dest(config.appPath))
});

gulp.task('clean', function (cb) {
del([
config.appPath + '/**'
], cb);
});

Now lets make sure our Stylus files are correctly inserted into the HTML: We’ll need gulp-inject for that.

1
npm install --save-dev gulp-inject
1
2
3
4
5
6
7
8
gulp.task('index', function () {
var target = gulp.src('src/html/**/*.html');
// It's not necessary to read the files (will speed up things), we're only after their paths:
var sources = gulp.src(['./app/**/*.js', './app/**/*.css'], {read: false});

return target.pipe(inject(sources, {ignorePath: '/app'}))
.pipe(gulp.dest('./app'));
});

Put placeholders in our index html file

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html>
<head>
<title>My index</title>
<!-- inject:css -->
<!-- project css files will go here... -->
<!-- endinject -->
</head>
<body>
Hello World!
</body>
</html>

Run gulp index and in the app directory the file becomes:

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html>
<head>
<title>My index</title>
<!-- inject:css -->
<link rel="stylesheet" href="/css/main.css">
<!-- endinject -->
</head>
<body>
Hello World!
</body>
</html>

External dependencies with Bower

Create bower file to track dependencies (install bower with sudo npm install -g bower if you don’t already have it).

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
mbp:~/dev/cwrky$ bower init
? name: cwrky
? version: 0.0.1
? description:
? main file:
? what types of modules does this package expose?
? keywords:
? authors: akshayrangnekar
? license: MIT
? homepage: https://github.com/akshayrangnekar/cwrky
? set currently installed components as dependencies? Yes
? add commonly ignored files to ignore list? Yes
? would you like to mark this package as private which prevents it from being accidentally published to the registry? Yes

{
name: 'cwrky',
version: '0.0.1',
homepage: 'https://github.com/akshayrangnekar/cwrky',
authors: [
'akshayrangnekar'
],
license: 'MIT',
ignore: [
'**/.*',
'node_modules',
'bower_components',
'test',
'tests'
]
}

? Looks good? Yes

Lets install font-awesome using bower

1
2
3
4
5
6
7
mbp:~/dev/cwrky$ bower install fontawesome --save
bower fontawesome#* cached git://github.com/FortAwesome/Font-Awesome.git#4.3.0
bower fontawesome#* validate 4.3.0 against git://github.com/FortAwesome/Font-Awesome.git#*
bower fontawesome#~4.3.0 install fontawesome#4.3.0

fontawesome#4.3.0 bower_components/fontawesome
mbp:~/dev/cwrky$

And bootstrap, which will in turn require jQuery

1
mbp:~/dev/cwrky$ bower install bootstrap --save

Run bower from gulp - install gulp-bower then:

1
2
3
4
5
6
bower = require('gulp-bower');

gulp.task('bower', function() {
return bower()
.pipe(gulp.dest(config.bowerDir));
});

Now we need our bower components injected into the HTML file too.

1
mbp:~/dev/cwrky$ npm install --save-dev main-bower-files

And in our build task:

1
2
3
4
5
6
7
8
9
gulp.task('build', ['bower', 'copy:styl', 'copy:html'], function () {
var target = gulp.src(config.appPath + '/index.html');
// It's not necessary to read the files (will speed up things), we're only after their paths:
var sources = gulp.src([config.appPath + '/scripts/**/*.js', config.appPath + '/css/**/*.css'], {read: false});

return target.pipe(inject(sources, {ignorePath: 'app', relative: true}))
.pipe(inject(gulp.src(bowerFiles(), {read: false}), {name: 'bower', relative: true}))
.pipe(gulp.dest(config.appPath));
});

Scripts

We need to do the same for the scripts now. Since I’ll be using a lot of angular.js scripts, worth introducing a couple of modules we’ll need for angular now. Install gulp-ng-annotate and gulp-angular-filesort.

1
2
mbp:~/dev/cwrky$ npm install --save-dev gulp-ng-annotate
mbp:~/dev/cwrky$ npm install --save-dev gulp-angular-filesort

The first module helps you undo the effects of a shortcut you’ve likely used in angular. If your services for example look like this:

1
2
angular
.module('angNewsApp').factory('Auth', function($firebaseAuth, $firebaseObject, FIREBASE_URL, $rootScope) {

gulp-ng-annotate will convert it to look like this:

1
2
3
4

angular
.module('angNewsApp').factory('Auth', ["$firebaseAuth", "$firebaseObject", "FIREBASE_URL", "$rootScope",
function($firebaseAuth, $firebaseObject, FIREBASE_URL, $rootScope) {

This is needed for minification later, as well as for (it appears empirically) for the filesort functionality.

The second module ensures that modules are imported in the correct order. To make it work correctly, don’t use a global for your module (e.g., app - but call angular.module() to acquire it).

Here’s the current gulpfile.js if you’re following along:

gulpfile.js.txtview raw
1
// Include gulp
var gulp = require('gulp');
var stylus = require('gulp-stylus');
var concat = require('gulp-concat');
var inject = require('gulp-inject');
var del = require('del');
var bowerFiles = require('main-bower-files')
    bower = require('gulp-bower');
var angularFilesort = require('gulp-angular-filesort'),
    ngAnnotate = require('gulp-ng-annotate');


var config = {

   appPath: './app',

   bowerDir: './bower_components'

}

gulp.task('copy:styl', function() {
  return gulp.src('src/styl/main.styl')
    .pipe(stylus())
    .pipe(gulp.dest(config.appPath + '/css'))
});

gulp.task('copy:html', function() {
  return gulp.src('src/html/**/*.html')
    .pipe(gulp.dest(config.appPath))
});

gulp.task('copy:scripts', function() {
  return gulp.src('src/scripts/**/*.js')
    .pipe(ngAnnotate())
    .pipe(gulp.dest(config.appPath + '/scripts'))
});

gulp.task('copy:bower', ['bower'], function() {
  return gulp.src(config.bowerDir + '/**/*.*')
    .pipe(gulp.dest(config.appPath + '/bower_components'))
});

gulp.task('bower', function() {
    return bower()

       .pipe(gulp.dest(config.bowerDir))
;
});

gulp.task('clean', function (cb) {
  del([
    config.appPath + '/**'
  ], cb);
});

gulp.task('build', ['copy:bower', 'copy:scripts', 'copy:styl', 'copy:html'], function () {
  var target = gulp.src(config.appPath + '/index.html');
  // It's not necessary to read the files (will speed up things), we're only after their paths:
  var sources = gulp.src([config.appPath + '/css/**/*.css'], {read: false});

  return target.pipe(inject(sources, {ignorePath: 'app', relative: true}))
        .pipe(inject(gulp.src(bowerFiles(), {read: false}), {name: 'bower'}))
        .pipe(inject(gulp.src(config.appPath + '/scripts/**/*.js') // gulp-angular-filesort depends on file contents, so don't use {read: false} here
            .pipe(angularFilesort())))
        .pipe(gulp.dest(config.appPath));
});

gulp.task('default', ['build']);

JSHint

1
2
mbp:~/dev/cwrky$ npm install --save-dev gulp-jshint
mbp:~/dev/cwrky$ npm install --save-dev jshint-stylish

Create new lint task and make it a dependency for copying JS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var jshint = require('gulp-jshint');
var stylish = require('jshint-stylish');

gulp.task('lint:js', function() {
return gulp.src('src/scripts/**/*.js')
.pipe(jshint())
.pipe(jshint.reporter(stylish));
});

gulp.task('copy:scripts', ['lint:js'], function() {
return gulp.src('src/scripts/**/*.js')
.pipe(ngAnnotate())
.pipe(gulp.dest(config.appPath + '/scripts'))
});

Watch for changes

1
2
3
4
gulp.task('watch', ['build'], function() {
gulp.watch('src/**/*.*', ['build']);
gulp.watch(config.bowerDir, ['build']);
});

Server

Install live reload functionality

1
mbp:~/dev/cwrky$ npm install --save-dev gulp-server-livereload

Wrap up the gulpfile:

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
// Include gulp
var gulp = require('gulp');
var stylus = require('gulp-stylus');
var concat = require('gulp-concat');
var inject = require('gulp-inject');
var del = require('del');
var bowerFiles = require('main-bower-files')
bower = require('gulp-bower');
var angularFilesort = require('gulp-angular-filesort'),
ngAnnotate = require('gulp-ng-annotate');
var jshint = require('gulp-jshint');
var stylish = require('jshint-stylish');
var server = require('gulp-server-livereload');
var changed = require('gulp-changed');


var config = {
appPath: './app',
bowerDir: './bower_components'
}

gulp.task('copy:styl', function() {
return gulp.src('src/styl/main.styl')
.pipe(stylus())
.pipe(gulp.dest(config.appPath + '/css'))
});

gulp.task('copy:html', function() {
return gulp.src('src/html/**/*.html')
.pipe(gulp.dest(config.appPath))
});

gulp.task('copy:scripts', ['lint:js'], function() {
return gulp.src('src/scripts/**/*.js')
.pipe(ngAnnotate())
.pipe(gulp.dest(config.appPath + '/scripts'))
});

gulp.task('copy:bower', ['bower'], function() {
return gulp.src(config.bowerDir + '/**/*.*')
.pipe(gulp.dest(config.appPath + '/bower_components'))
});

gulp.task('lint:js', function() {
return gulp.src('src/scripts/**/*.js')
.pipe(jshint())
.pipe(jshint.reporter(stylish));
});

gulp.task('bower', function() {
return bower()
.pipe(gulp.dest(config.bowerDir));
});

gulp.task('clean', function (cb) {
del([
config.appPath + '/**'
], cb);
});

gulp.task('watch', ['build'], function() {
gulp.watch('src/**/*.*', ['build']);
gulp.watch(config.bowerDir, ['build']);
});

gulp.task('server', ['build'], function() {
gulp.watch('src/**/*.*', ['build']);
gulp.watch(config.bowerDir, ['build']);
gulp.src('app/', { base: 'app' })
.pipe(server({
livereload: true,
directoryListing: false,
open: true
}));

});


gulp.task('build', ['copy:bower', 'copy:scripts', 'copy:styl', 'copy:html'], function () {
var target = gulp.src(config.appPath + '/index.html');
// It's not necessary to read the files (will speed up things), we're only after their paths:
var sources = gulp.src([config.appPath + '/css/**/*.css'], {read: false});

return target.pipe(inject(sources, {ignorePath: 'app', relative: true}))
.pipe(inject(gulp.src(bowerFiles(), {read: false}), {name: 'bower'}))
.pipe(inject(gulp.src(config.appPath + '/scripts/**/*.js') // gulp-angular-filesort depends on file contents, so don't use {read: false} here
.pipe(angularFilesort()), {ignorePath: 'app', relative: true}))
.pipe(gulp.dest(config.appPath));
});

gulp.task('default', ['build']);