Writing a plugin

If you plan to create your own Gulp plugin, you will save time by reading the full documentation.

What it does

Streaming file objects

A gulp plugin always returns a stream in object mode that does the following:

  1. Takes in vinyl File objects
  2. Outputs vinyl File objects (via transform.push() and/or the plugin's callback function)

These are known as transform streams (also sometimes called through streams). Transform streams are streams that are readable and writable; they manipulate objects as they're being passed through.

All gulp plugins essentially boil down to this:

var Transform = require('transform');

module.exports = function() {
  // Monkey patch Transform or create your own subclass,
  // implementing `_transform()` and optionally `_flush()`
  var transformStream = new Transform({objectMode: true});
  /**
   * @param {Buffer|string} file
   * @param {string=} encoding - ignored if file contains a Buffer
   * @param {function(Error, object)} callback - Call this function (optionally with an
   *          error argument and data) when you are done processing the supplied chunk.
   */
  transformStream._transform = function(file, encoding, callback) {
    var error = null,
        output = doSomethingWithTheFile(file);
    callback(error, output);
  };

  return transformStream;
};

Many plugins use the through2 module to simplify their code:

var through = require('through2');    // npm install --save through2

module.exports = function() {
  return through.obj(function(file, encoding, callback) {
    callback(null, doSomethingWithTheFile(file));
  });
};

The stream returned from through() (and this within your transform function) is an instance of the Transform class, which extends Duplex, Readable (and parasitically from Writable) and ultimately Stream. If you need to parse additional options, you can call the through() function directly:

  return through({objectMode: true /* other options... */}, function(file, encoding, callback) { ...

Supported options include:

  • highWaterMark (defaults to 16)
  • defaultEncoding (defaults to 'utf8')
  • encoding - 'utf8', 'base64', 'utf16le', 'ucs2' etc. If specified, a StringDecoder decoder will be attached to the stream.
  • readable {boolean}
  • writable {boolean}
  • allowHalfOpen {boolean} If set to false, then the stream will automatically end the readable side when the writable side ends and vice versa.

Modifying file content

The function parameter that you pass to through.obj() is a _transform function which will operate on the input file. You may also provide an optional _flush function if you need to emit a bit more data at the end of the stream.

From within your transform function call this.push(file) 0 or more times to pass along transformed/cloned files. You don't need to call this.push(file) if you provide all output to the callback() function.

Call the callback function only when the current file (stream/buffer) is completely consumed. If an error is encountered, pass it as the first argument to the callback, otherwise set it to null. If you have passed all output data to this.push() you can omit the second argument to the callback.

Generally, a gulp plugin would update file.contents and then choose to either:

  • call callback(null, file) or
  • make one call to this.push(file)

If a plugin creates multiple files from a single input file, it would make multiple calls to this.push() - eg:

module.exports = function() {
  /**
   * @this {Transform}
   */
  var transform = function(file, encoding, callback) {
    var files = splitFile(file);
    this.push(files[0]);
    this.push(files[1]);
    callback();
  };

  return through.obj(transform);
};

The gulp-unzip plugin provides a good example of making multiple calls to push(). It also uses a chunk transform stream with a _flush() function within the Vinyl transform function.

Vinyl files can have 3 possible forms for the contents attribute:

  • Streams
  • Buffers
  • Empty (null) - Useful for things like rimraf, clean, where contents is not needed.

A simple example showing how to detect & handle each form is provided below, for a more detailed explanation of each approach follow the links above.

var PluginError = require('gulp-util').PluginError;

// consts
var PLUGIN_NAME = 'gulp-example';

module.exports = function() {
    return through.obj(function(file, encoding, callback) {
        if (file.isNull()) {
            // nothing to do
            return callback(null, file);
        }

        if (file.isStream()) {
            // file.contents is a Stream - https://nodejs.org/api/stream.html
            this.emit('error', new PluginError(PLUGIN_NAME, 'Streams not supported!'));

            // or, if you can handle Streams:
            //file.contents = file.contents.pipe(...
            //return callback(null, file);
        } else if (file.isBuffer()) {
            // file.contents is a Buffer - https://nodejs.org/api/buffer.html
            this.emit('error', new PluginError(PLUGIN_NAME, 'Buffers not supported!'));

            // or, if you can handle Buffers:
            //file.contents = ...
            //return callback(null, file);
        }
    });
};

Note: When looking through the code of other gulp plugins (and the example above), you may notice that the transform functions will return the result of the callback:

return callback(null, file);

...don't be confused - gulp ignores any return value of your transform function. The code above is simply a short-hand form of:

if (someCondition) {
  callback(null, file);
  return;
}
// further execution...

Useful resources

Sample plugins

About streams

If you're unfamiliar with streams, you will need to read up on them:

Other libraries that are not file manipulating through streams but are made for use with gulp are tagged with the gulpfriendly keyword on npm.

results matching ""

    No results matching ""