Advanced Gulp
Just over a year ago I wrote an article about adopting gulp as a new task runner. My opinion of gulp has not changed. I still find it to be the best for me. But, I have learnt more about it. The following are some tips and pointers for using gulp that should enable you to put together an awesome gulpfile.
TL;DR I used gulp for a year and put together a better gulp boilerplate using tips and tricks I’ve learnt. It can be seen here. It gets set up with a livereload static server and stylus, babel and pug compilation.
Task Dependencies permalink
A basic but sometimes overlooked or unknown feature is the ability to set dependencies on tasks. This is great for say a deployment task where we need to ensure that our sources are built before deploying them to a server.
Consider two tasks
gulp.task('build', ['javascript::build', 'css::build']);
gulp.task('deploy', function(e){
return gulp.src(sources.build)
.pipe(deploy());
});
Instead of manually running these tasks in order we can explicitly state that our deploy task will always be dependant on the build task running.
gulp.task('deploy', ['build'], function(e){
return gulp.src(sources.build)
.pipe(deploy());
});
Config All the Things permalink
One of the things that drew me to using gulp was getting away from having to write and remember all the different configurations for different grunt plugins.
However, it is a good idea to extract commonly used arguments such as glob paths and plugin options from your gulp tasks and place them in an external config file to keep your gulpfile nice and readable.
For example, I found myself heading my gulpfiles with various path definition objects such as
var gulp = require('gulp'),
paths: {
sources: {
coffee: 'src/coffee/**/*.coffee',
docs: 'src/jade/*.jade',
jade: 'src/jade/**/*.jade',
stylus: 'src/stylus/**/*.stylus',
overwatch: 'out/**/*.{html,js,css}'
},
destinations: {
js: 'out/js/',
css:'out/css/'
}
};
This isn’t nice and does nothing for readability so extract this into a separate config file and import it into your gulpfile at run time.
/* gulp-config.js */
module.exports = {
paths: {
sources: {
js: 'src/js/**/*.js',
docs: 'src/jade/*.jade',
pug: 'src/pug/**/*.pug',
stylus: 'src/stylus/**/*.stylus',
overwatch: 'out/**/*.{html,js,css}'
},
destinations: {
js: 'out/js/',
css: 'out/css/'
}
}
};
The nice thing is you won’t have to alter your tasks if you do something like
var gulp = require(‘gulp’),
gConfig = require(‘./gulp-config’),
sources = gConfig.paths.sources,
destinations = gConfig.paths.destinations;
env Flag permalink
If you are familiar with gulp-util you may know this trick already. Using gulp-util you can set environment variables/flags for your tasks which enables you to do things like alter the default task set,change task behaviour or output location based on various flags.
gulp.task('script::compile', function(e){
return gulp.src(sources.js)
.pipe(babel())
.pipe(concat('app.js'))
.pipe(gulp.dest(destinations.js))
.pipe(uglify())
.pipe(rename({suffix: '.min'}))
.pipe(gulp.dest(destinations.js));
});
For an example, we have a task that compiles our script sources. Sometimes we want to see the stats for our output such as file size. Other times we may be compiling a distribution version of our source and therefore we want to output our compiled sources to a different destination. We do not want to repeat our task three times just to get the different behaviour. Using environment flags we can alter our task slightly but increase the lifting it is capable of.
We start by getting our flag values at runtime
var gulp = require('gulp'),
gUtil = require('gulp-util'),
isDist = (gUtil.env.dist) ? true: false,
isStat = (gUtil.env.stat) ? true: false;
Then we simply make use of those flags within our task
gulp.task('script::compile', function(e) {
return gulp.src(sources.js)
.pipe(babel())
.pipe(concat('app.js'))
.pipe(isStat ? size({showFiles: true}): gUtil.noop())
.pipe(gulp.dest(isDist ? destinations.dist: destinations.js))
.pipe(uglify())
.pipe(rename({suffix: '.min'}))
.pipe(isStat ? size({showFiles: true}): gUtil.noop())
.pipe(gulp.dest(isDist ? destinations.dist: destinations.js));
});
If a flag isn’t set we simply use gulp-util’s handy noop() function.
DRY permalink
Most developers will be familiar with the DRY(don’t repeat yourself) principle. It will go without saying but is worth mentioning that your tasks in gulp should be as modular and generic as possible for your needs.
If two tasks do the same thing for the most part, look to extract the repetition into a dependant task or use flags to combine the tasks into one task.
Knowing Your Plugins permalink
The ecosystem for gulp has grown immensely in the last 12 months. One of grunts selling points was the fact that it had a much more mature ecosystem of plugins as opposed to gulp.
I don’t feel this is the case now.
Plugins can be found for the majority of tasks. It’s worth searching the registry if there is something you’re looking for.
Notable plugins for myself are;
- Utilities — gulp-util, gulp-tap, gulp-gh-pages, gulp-load-plugins
- Compilation — gulp-jade, gulp-coffee, gulp-stylus, gulp-less, gulp-sass
- Optimisation — gulp-autoprefixer, gulp-uglify, gulp-minify-css
- Manipulation — gulp-wrap, gulp-concat
Where plugins don’t exist for what you want to do, there is normally a reason. One of the biggest reasons being that you can most likely get the desired behaviour from writing some JavaScript and using the relevant node packages inside of a task instead of a plugin. This is the power of gulp, being able to do what you want.
Two popular examples are the use of browser-sync or del. However, in these cases there are existing plugins. gulp-connect actually handles what you can do with browser-sync but is actually black listed as a supported plugin for the reason that it is seen as redundant. I’ll talk a little about this with regards to writing your own plugins.
Loading Plugins permalink
If there is one plugin you need to know about it is gulp-load-plugins.
This plugin will lazy load your gulp plugins and enables you to go from writing this;
var gulp = require('gulp'),
util = require('gulp-util'),
jade = require('gulp-jade'),
coffee = require('gulp-coffee'),
stylus=require('gulp-stylus');
to this;
var gulp = require('gulp'),
plugins = require('gulp-load-plugins')();
To invoke your plugins within tasks you simply prefix them with the variable you named the gulp-load-plugins plugin.
gulp.task('markup::compile', function(){
return gulp.src(sources.pug)
.pipe(plugins.pug())
.pipe(gulp.dest(destinations.docs));
});
To rename a loaded plugin, you can define names within the gulp-load-plugins declaration. For example; renaming gulp-autoprefixer to prefix.
var gulp = require('gulp'),
plugins = require('gulp-load-plugins')({
'gulp-autoprefixer': 'prefix'
});
This can then be extracted into our config file.
Task Names and Prefixing/Suffixing permalink
Try and stick to a consistent and verbose naming standard for your tasks. Also, try and be generic with the names. I personally like to use suffixes and try to namespace tasks.
It’s likely the main types of source you will be dealing with are scripts, styles and markup. So name your tasks that way! 😀
This makes it easier for newcomers who just want to compile the scripts for example.
This
gulp.task('javascript', () => ...)
could be written in various ways
gulp.task('js', () => ...)
gulp.task('JavaScript', () => ...)
gulp.task('compileJs', () => ...)
And the list goes on… I personally try and stick to the following pattern;
<what I am running a task on>:<what the task is doing>
So if I’m compiling scripts;
gulp.task('scripts:compile', () => ...)
Then if I’m watching or doing something else with them I just use that same pattern. It can make things a lot easier for newcomers and it’s just plain easier to remember/maintain.
At all costs PLEASE avoid using task names that include the preprocessor of choice in them if using one. This becomes hard to maintain if later down the line you decide that you no longer want to use it and want to change.
For example, I have several tasks using less
and then I change to sass
. I then need to go and change all instances where less
was referenced. Save yourself the hassle from the start and use something like styles:compile
🎉
Bonus: Writing Plugins and Plugin Politics permalink
Although not directly about using gulp I figured writing plugins for gulp was worth mentioning and the politics surrounding them.
There are some strong guidelines to writing plugins for gulp. They can be seen here.
Some will say that they are quite strict but it isn’t without reason. Gulp doesn’t want a polluted ecosystem of duplicated plugins or plugins that just wrap around current node packages. There is maintained gulp plugin blacklist concerned with this.
It is almost advised that in most cases writing a plugin is a last resort. If it can be done with node then you don’t need a plugin. However, there does seem to be a lot of politics around whether plugins are required or not and whether they should be blacklisted or not.
One example is gulp-connect. It’s a popular plugin that sets up a static livereload server. It is blacklisted because you can use express, livereload etc. directly. However, this isn’t as straight forward to set up for most people and I myself am guilty of using gulp-connect to save time. The alternative is to use browser-sync which is actually very easy to set up and behaves in the same way as gulp-connect. But, just because a plugin is blacklisted doesn’t mean you can’t use it.
Also, when do you draw the line at what should be made into a plugin? You could argue that the majority of even non blacklisted plugins could be written using node packages or with the use of other gulp plugins.
I recently wrote my own plugin — gulp-template-store.
It’s a plugin for taking HTML templates and generating a JST file of lodash templates for those templates. It was inspired by another plugin that does something similiar. However, that plugin hasn’t been maintained for over a year.
By the blacklist rules, my plugin should be blacklisted because of duplicate behaviour but hasn’t because I raised an issue putting across the case that mine is actively maintained.
But you could also argue that even then it should be blacklisted because templating files could be created with a combination of other plugins;
var gulp = require('gulp'),
tmpl = require('gulp-template'),
_ = require('lodash'),
tap = require('gulp-tap'),
path = require('path'),
sources = {
html: 'src/html/**/*.html'
},
destinations = {
templates: './tmp/'
},
tmplNamespace = 'tmpl',
tmplRoot = 'src/html/';
// A task when given a source of HTML files, will create a namespaced JST file of your templates.
gulp.task('templates:compile', function(event) {
return gulp.src(sources.html)
.pipe(tmpl.precompile())
.pipe(tap(function(file) {
var tmplStr = '"<%= name %>": <%= contents %>,',
fName = file.path.slice(file.path.indexOf(tmplRoot) + tmplRoot.length),
newCont = _.template(tmplStr)({name: fName, contents: file.contents.toString()});
file.contents = new Buffer(newCont);
}))
.pipe(concat('templates.js'))
.pipe(wrap('var ' + tmplNamespace + ' = {<%= contents %>};'))
.pipe(gulp.dest(destinations.templates));
});
It’s not the prettiest, but that would actually achieve the same result almost with some tweaking of the options. It makes our gulpfile messy though and we want it to be easily comprehensible by other developers that may use it.
This is where I think it’s hard to say when a plugin should be blacklisted or not. Some tasks will be simple to write with node packages or a combination of gulp plugins but your tasks should remain easy to read and understand.
My advice would be that in most cases there is going to be a plugin already available for your needs and if not there is most likely a node package ready and waiting. I wrote gulp-template-store as I wanted to learn how to write a plugin and for me I think it is certainly easier to use than writing a large task as above.
Summary permalink
It has been over a year since I started using gulp and it has shown no signs of letting me down. I’m still picking up new things I can do with it but that’s the beauty of gulp in that it has no real restrictions on what you can do.
Some of the tips and tricks mentioned should help you to put together a more powerful and comprehensible gulpfile for your projects.
Be sure to check out the gulp-boilerplate I’ve put together if you’d like a starting point.