Gulp

Home

This page links to:

webpack.html typescript.html

These pages link to here:

spiderweb.html

Table of contents

  1. Installation
  2. gulpfile.js
  3. Tasks
    1. Signaling task completion
    2. Running tasks synchronously
    3. Privacy
    4. Composition
  4. Using files
    1. src()
    2. pipe()
    3. dest()
  5. Globs
    1. Single asterisks
    2. Double asterisks
    3. Exclamation marks
    4. Glob base
  6. Vinyl
  7. Plugins

Gulp is a build system - it's a system that helps you automate tasks that you need to do to build your app.

Installation#

npm install --global gulp-cli # installs the CLI tool
npm install --save-dev gulp # installs Gulp itself

gulpfile.js#

Gulp, like Webpack, uses a config file; Gulp's is always called gulpfile.js.

It's made up of a series of tasks: each task does something different to build a bit of your app' this is the simplest possible task:

function defaultTask(cb) {
  // place code for your default task here
  cb();
}

exports.default = defaultTask

The gulpfile is ran with gulp [task1] [task2], gulp will run the default task.

You can split up the tasks into different files, by making a gulpfile.js directory, moving your previous JS file into that & renaming it index.js, and importing your different tasks.

Tasks#

Each task in a Gulpfile uses some of the Gulp APIs like src() or dest() to do something, like bundle many CSS files together, use Webpack to bundle and transpile some JS, or version different files.

gulp --tasks will list all of your tasks.

Each task is asynchronous - they don't run synchronously, this is quite important: if you think they run synchronously, you'd expect gulp taskA taskB taskC to run taskA, then taskB, then taskC, but that doesn't happen. Make sure that if you need your tasks to run in a particular order, for example by merging their outputs into a manifest file, that you make them run synchronously.

Each task is an asynchronous function - the first parameter is an error-first callback, a common Node pattern where the callback is called with the error as the first parameter, if any, or null if there isn't an error.
In the simple Gulpfile example above, cb is the callback function:

function defaultTask(cb) {
  // place code for your default task here
  cb();
}

Signaling task completion#

The task can return nothing, a Node stream, promise, event emitter, child process, or an observable.

All of the above options are different ways Node libraries handle asynchronous functions; Gulp tasks can use any of them.

When a task finishes, Gulp needs to know if it should stop or continue - if your task returns something and an error is thrown, Gulp will stop straight away and show the error; otherwise it'll continue.

If your task doesn't return something, you need to specifically tell Gulp the task's finished, by calling the callback function, with a new Error(), or nothing (if it was successful).

If you get a warning like

"Did you forget to signal async completion?"

you need to do one of the above things - returning a stream, calling the callback, etc.

Running tasks synchronously#

Use series()

Privacy#

Tasks can either be public or private

Composition#

If you want to compose tasks together, you can use series() to run them one-after-another, or parallel() to run them concurrently:

const { series } = require('gulp');
function transpile(cb) {
  // body omitted
  cb();
}

function bundle(cb) {
  // body omitted
  cb();
}

exports.buildJS = series(transpile, bundle);

const { parallel } = require('gulp');
function javascript(cb) {
  // body omitted
  cb();
}

function css(cb) {
  // body omitted
  cb();
}

exports.buildAll = parallel(javascript, css);

You can nest them as much as you want:

exports.build = series(
    clean,
    parallel(
        cssTranspile,
        series(jsTranspile, jsBundle)
    ),
    parallel(cssMinify, jsMinify),
    publish
);

The Gulp docs says this:

When a composed operation is run, each task will be executed every time it was referenced. For example, a clean task referenced before two different tasks would be run twice and lead to undesired results. Instead, refactor the clean task to be specified in the final composition.

and to change this

const css = series(clean, ...);
const javascript = series(clean, ...);
exports.build = parallel(css, javascript);

to this

const css = series(...);
const javascript = series(...);
exports.build = series(clean, parallel(css, javascript));

Using files#

Gulp's main API are the src(), dest() and pipe(), called like gulp.src() or src(), depending on how you import it.

const { src, dest } = require('gulp');
const babel = require('gulp-babel');

exports.default = function() {
  return src('src/*.js')
    .pipe(babel())
    .pipe(dest('output/'));
}

You can call src() or dest() in the middle of a pipeline to add files halfway through (e.g. to transpile some typescript, then uglifying it and normal JS), or to write intermediate states to the filesystem (e.g. to create unminified and minified files with the same pipeline).

src()#

src(globs, [options])

src() is given a glob to read from the file system, and makes a Node stream that produces Vinyl objects. It finds all the files that match the glob, and reads them into memory to pass through the stream, one file at a time.

A stream is an abstract interface for working with streaming data in Node.js

The stream that src() makes should be returned, to signal task completion.

It has 3 modes:

Each file is represented by a Vinyl instance.

It has useful options, like base, which explicitly sets the glob base on the Vinyl objects that are created.

pipe()#

pipe() is generally where plugins will transform the files in the stream - each call to pipe() is given one file in the stream.

It's used to chain different Transform or Writable streams together:

dest()#

dest(folder, [options])

You give dest() an output directory string, and it makes a Node stream that consumes Vinyl objects. It's normally used to signal task completion.
When it gets a file through the stream (as a Vinyl, passed through pipe()s), it writes the file out to the filesystem at that directory.

You could also use symlink(), which makes links rather than files.

Globs#

src() takes in either 1 string glob, or an array of globs to find files for your pipeline to work on - if it's given an array, it goes through them from the start to the end; this is useful for negative globs.

A glob is a string of characters used to match filepaths, that can include wildcards, like in Unix paths.

The path separator is always /, even on Windows.

A segment is everything between path separators.

Don't use Node's path methods, like path.join, to create globs - on Windows, it makes an invalid glob because Node uses \ as the path separator.
Avoid __dirname, __filename, or process.cwd() for the same reason.

Single asterisks#

* matches any series of characters within a single segment, so inside 1 directory.

Double asterisks#

** matches any series of characters within multiple segments, so inside nested directories, not just 1 directory:
./resources/assets/js/**/*.js matches any JS files inside the js folder, no matter how deeply nested they are.

Exclamation marks#

! removes any files that otherwise would've been included in the stream (so it must come after a non-negative glob).
Here, any files in the designSystem directory aren't included in the stream:

gulp.src([
    "./resources/assets/css/**/*.css",
    "!./resources/assets/css/designSystem/**/*",
], {base: "./resources/assets"})

Glob base#

The glob base is the bit of the glob before any special characters: in /src/js/**.js, it's /src/js/.
Each Vinyl instance that src() makes has the glob base set as their base property.

The base must be the same for all globs used by src() - if they're different, you need to set it explicitly.

When the Vinyl is written to the file system with dest(), the base gets removed from the output path to preserve directory structures.

If you want to change what the glob base is, use the base option in src():

gulp.src([
    "./resources/assets/css/**/*.css",
    "!./resources/assets/css/designSystem/**/*",
], {base: "./resources/assets"})

I needed to set the base here explicitly, because the two globs have different bases otherwise.

You also might want to change the base to put the output files in a different directory:

gulp.src(
    "./resources/assets/images/**/*", 
    {base: "./resources/assets"}
)

Here, this avoids putting the images directly into wherever gulp.dest() specifies (which will use the same, single, path, in a versioning task), instead putting them inside the images folder inside where gulp.dest() specifies.

Vinyl#

A Vinyl is a metadata object that describes a file - the main properties are path and contents - core aspects of a file on your file system.

Vinyl objects can describe local or remote files.

Plugins#

Gulp plugins are Node Transform Streams that transform files in a pipeline.
They can change the filename, metadata, or contents of every file that passes through the stream.

top