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 | mbp:~/dev/cwrky$ npm init |
Install gulp locally1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18mbp:~/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 structure1
2
3
4mbp:~/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-stylus1
2
3
4
5
6
7
8
9
10
11mbp:~/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
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-concat1
2
3
4
5
6
7mbp:~/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
8var 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 | var del = require('del'); |
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 | gulp.task('index', function () { |
Put placeholders in our index html file
1 | <!DOCTYPE html> |
Run gulp index
and in the app directory the file becomes:
1 | <!DOCTYPE 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 | mbp:~/dev/cwrky$ bower init |
Lets install font-awesome using bower
1 | mbp:~/dev/cwrky$ bower install fontawesome --save |
And bootstrap, which will in turn require jQuery1
mbp:~/dev/cwrky$ bower install bootstrap --save
Run bower from gulp - install gulp-bower
then:1
2
3
4
5
6bower = 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
9gulp.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 | mbp:~/dev/cwrky$ npm install --save-dev gulp-ng-annotate |
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 | angular |
gulp-ng-annotate will convert it to look like this:
1 |
|
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:
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 | mbp:~/dev/cwrky$ npm install --save-dev gulp-jshint |
Create new lint task and make it a dependency for copying JS1
2
3
4
5
6
7
8
9
10
11
12
13
14var 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 | gulp.task('watch', ['build'], function() { |
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']);