gulp is the new black. It’s quickly supplanting Grunt for JavaScript builds because it’s faster and simpler. When we replaced our Gruntfile with a gulpfile on one of our projects, our build script sped up by an order of magnitude and became much more comprehensible.
Because gulp is so new, its suite of plugins is much smaller. Granted it’s easy to use many basic node modules without a gulp plugin, like:
var rimraf = require('rimraf');
gulp.task('clean', function (cb) {
rimraf('./build', cb);
});
But in many other cases a plugin either saves code duplication between projects or makes it simpler to perform a conventional action. How hard is it to create a gulp plugin?
Example plugin
With no frills, here are 14 lines (plus a bunch of comments) to create a plugin that calls SCSS-Lint (a linter for Sass, a CSS variant) for each received file. See below for a detailed explanation.
// child_process is used to spawn the scss-lint binary
var child_process = require('child_process');
// map-stream is used to create a stream that runs an async function
var map = require('map-stream');
// gulp-util is used to created well-formed plugin errors
var gutil = require('gulp-util');
// The main function for the plugin – what the user calls – should return
// a stream.
var scssLintPlugin = function() {
// Run the scss-lint binary in a separate process, inheriting all stdio
// from the gulp process. Errors and stdout will be logged by gulp.
function spawnScssLint(filePaths) {
return child_process.spawn('scss-lint', filePaths, {
stdio: 'inherit'
});
}
// Create and return a stream that, for each file, asynchronously
// processes the file through scss-lint and calls the callback method
// when complete.
return map(function(file, cb) {
var lint = spawnScssLint([file.path]);
// When scss-lint closes, check its status code for an error.
// SCSS-Lint defines status code 65 as indicating a lint warning or error.
// We don't want lint problems to kill gulp, so status code 65 is not
// transformed into a stream error.
lint.on('close', function(code) {
if (code && 65 !== code) {
var error = gutil.PluginError('gulp-scsslint', 'SCSS-Lint returned ' + code);
}
// Call the callback function with an error (which should be falsey
// if no error occurred) and the given file.
cb(error, file);
});
});
};
// Export the plugin main function
module.exports = scssLintPlugin;
This plugin can then be used from a gulpfile like:
var gulp = require('gulp');
var scsslint = require('./gulp-scsslint-simple.js');
gulp.task('lint', function() {
gulp.src('styles/*.scss').
pipe(scsslint());
}
The "lint" task will call SCSS-Lint for each file in the stream. SCSS-Lint’s default output format will be used to log lint warnings and errors to gulp’s stdout.
Explanation
What’s going on in this file?
In line 22, we create a stream that asynchronously handles each file: return map(function(file, cb) { ... })
- this stream is returned from our plugin
- this stream’s callback is given files one at a time
- this stream’s callback is itself given a callback, which should be invoked when processing the file is complete
In lines 14-16, we call the scss-lint binary using the child_process module: return child_process.spawn('scss-lint', filePaths, { stdio: 'inherit' });
- we pass the given file’s path to scss-lint as a command line option
- scss-lint is told to redirect its IO to ‘inherit’, meaning the gulp process receives its stdout and stderr
- child_process.spawn() returns a process object
In line 29, we listen for an scss-lint "close" event that indicates the child process has quit: lint.on('close', function(code) { ... })
In lines 30-32, we handle any errors returned by scss-lint:
if (code && 65 !== code) {
var error = gutil.PluginError('gulp-scsslint', 'SCSS-Lint returned ' + code);
}
- if the scss-lint exit code indicates an error, we create a plugin error that gulp can use
- (we ignore scss-lint exit code 65, which indicates that scss-lint encountered a lint warning or error)
In line 36, we tell gulp that our work is done by invoking the callback: cb(error, file);
- if the callback method is not invoked, gulp will hang on this plugin
- if the callback method is called before scss-lint is complete then scss-lint may not complete before gulp does
- passing in a non-falsey error to the callback will cause gulp to handle the error
- passing in the file to the callback allows the next gulp plugin to use the same file for additional work
In line 42, we make our main method available to the user: module.exports = scssLintPlugin;
What’s missing?
Nothing additional is necessary, but other features might be useful. For example:
- Send lint data (e.g. lint warnings) along with the emitted files
- Optionally handle lint warnings and errors in gulp, e.g. to cancel a CI build
- Call scss-lint with more than one file at a time to speed up the plugin
- Better error handling, such as checking for a missing scss-lint binary
I’ve expanded on this simple example with a complete gulp-scsslint plugin that covers these features and more.