Writing gulp tasks

Now that you know how to get gulp up and running in VS Code, and how to export tasks from your gulp file, let’s talk about tasks themselves. What are they, what are they good for, and how to write them.

Gulp – being a task automation tool – is all about tasks. Anything you do about programming, which is not writing the code itself, can be considered a task, and can very likely be automated. Some tasks are automated already, like compiling your code, building your app. In case of AL development, there are many more already automated tasks, like creating a demo workspace or deploying your app to NAV. If you can automate them, tasks are immensely powerful because they save time and eliminate error.

If you are a web developer, your tasks can include bundling your source files and minifying them. For me, when building pre-2018 control add-ins, most important tasks were bundling source files, zipping the resource file, deploying a control add-in and importing the control add-in using PowerShell. All of these I have automated using gulp, and I’ll explain every single one of them in this series.

But for now, let’s simply take a look at how to write gulp tasks.

The most important thing to understand about gulp tasks is that they are not synchronous. They are asynchronous. When you invoke an asynchronous function, your code goes on, and if you need to clean, or synchronize some execution, you somehow must signal the completion of your function, so that other code that depends on your completion can happen. You’ve seen in my first post about gulp how to signal completion in the simplest of ways: using the error-first callback:

function helloWorldTask(callback) {
    console.log("Hello, World!");
    callback();
}

However, that’s not the only way to signal the end of a task. There are a few others, too.

One of a popular ways to signal an end of a task is using promises. A promise is an object that performs an asynchronous task, and promises to pass the control to whomever cars to listen, once the task is done. If your task returns a promise, gulp also natively understands it:

function helloWorldPromise() {
    return new Promise(fulfill => {
        console.log("Hello, World!");
        fulfill();
    });
}

You may say that using promises has no obvious advantage over using the error-fist callback, but you’d be wrong. Keep in mind that gulp tasks are simple JavaScript functions. A task function that returns a promise is far more versatile than a function that uses the error-first callback. Consider this example:

function helloMercury() {
    return new Promise(fulfill => {
        console.log("Hello, Mercury!");
        fulfill();
    });
}

function helloVenus() {
    return new Promise(fulfill => {
        console.log("Hello, Venus!");
        fulfill();
    });
}

function helloWorld() {
    return new Promise(fulfill => {
        console.log("Hello, World!");
        fulfill();
    });
}

function helloSolarSystem() {
    console.log("Hello, solar system!");
    return helloMercury().then(helloVenus).then(helloWorld);
}

module.exports.helloMercury = helloMercury;
module.exports.helloVenus = helloVenus;
module.exports.helloWorld = helloWorld;
module.exports.helloSolarSystem = helloSolarSystem;

Here, thanks to promises, you were able to use each function individually as a task, but also to chain them in a promise chain that becomes a task of its own. Very handy.

Now that we are talking promises, when your functions are involving promises, you can take advantage of async-await pattern, and change your helloSolarSystem task into an async function, like this:

async function helloSolarSystem() {
    console.log("Hello, solar system async!");
    await helloMercury();
    await helloVenus();
    await helloWorld();
}

Depending on your task’s complexity, this syntax is arguably easier to read. In both two previous cases, the execution flow – and obviously the output – are exactly the same:

Keep in mind that in JavaScript, async-await is nothing but syntactic sugar for Promises. Every function that returns a promise can be awaited from an async function, and if nothing else, then this should be the strongest argument to prefer promises over error-first callback pattern.

There are a couple more patterns that you can use, like event emitter, child process, or observable, but they are less frequently used and less readily useful as promises.

However, probably the most important pattern for gulp is definitely the stream pattern, where your gulp task function returns a Node.js stream object. This is a very versatile and complex object (as you can see from its documentation), but good news is – you don’t need to really fully understand streams to be able to use them with gulp. You merely need to understand that streams can read, write, and transform data, and that you can pipe their content onto other streams.

Let’s start simple. The most common stream in gulp is the stream containing files from the filesystem. Remember, you are automating tasks, which involves automating something you do to your project files, which in essence means that you’ll have to read some source files. Streams can do this for you. Gulp simplifies this by providing the src method which reads the specified files and returns a readable stream containing these files.

The following example shows how to read files from the disk using gulp.src method:

function readFiles() {
    return gulp.src("src/*.js");
}

See? You don’t have to signal completion or fulfil a promise or await on another promise to resolve. By simply returning a stream, you are indicating to gulp that the task is done, and gulp can listen to the stream’s finish event to occur.

However, if you export this function as a task and then run that task, seemingly nothing will happen. The function simply reads all .js files from directory src, and returns them as a stream that you can use to further manipulate it.

A typical manipulation that you will do in gulp is to copy files to an output directory. Simply piping your stream onto the gulp.dest method will copy your files into the specified destination directory. For example:

function copyFiles() {
    return gulp
        .src("src/*.js")
        .pipe(gulp.dest("dest"));
}

This task performs two operations. First, it reads all .js files from the src directory, and then pipes that stream onto the gulp.dest method, which itself creates a stream that will write the piped contents into the dest directory. If you now create src and dest directorys in the root of your workspace, add a couple of .js files to the src directory, and then run the copyFiles task, you’ll see that gulp copied your source files to your destination directory. Since the pipe method itself returns a stream, you can further pipe the contents of the stream onto another stream.

The following example will copy the source contents into two separate directories:

function copyFiles() {
    return gulp
        .src("src/*.js")
        .pipe(gulp.dest("dest1"))
        .pipe(gulp.dest("dest2"));
}

Run that, and you’ll see that now both your dest1 and dest2 directorys contain your source files.

Understanding how gulp.src and gulp.dest methods work is much more important than understanding the intrinsic details of Node.js streams, so let’s take a look at them closer.

First of all, the src method does not merely return a stream. If you use it at the beginning of a pipeline, it will read the files and forward them to a stream. However, if you use it in the middle of a pipeline, it will append files to the stream. This example illustrates that nicely:

function copyFiles() {
    return gulp
        .src("src/*.js")
        .pipe(gulp.dest("dest1"))
        .pipe(gulp.src("src/*.css"))
        .pipe(gulp.dest("dest2"));
}

Let’s deconstruct what this task does:

  • It reads all .js files from the src directory
  • It pipes these files into the dest1 destination directory
  • It reads all .css files from the src directory, and appends them to the stream
  • It pipes all these files (.js and .css) into the dest2 destination directory

The first parameter you pass to the src method is called glob. A glob is a wildcard pattern used to match files. For example, ‘src/.js’ matches all .js files that are direct children of the src directory; ‘src//.js’ will match all .js files that are children of any child directory of the src directory; ‘src//.js’ will match all .js files anywhere under src directory and its subdirectorys at any depth. Globs can be combined in arrays, so [‘/.css’, ‘/.js’] will match any .js or .css file anywhere under the workspace root. Globs can also be negative, so [‘/*.css’, ‘**/*.js’, ‘!dest/’] will match all .css and .js files in the workspace, except those under the dest directory. If you want to learn more how to use globs with gulp, check it here, or if you want to understand deep down what they are and what you can do with them, check it here.

The second parameter of the src method is optional, and it contains various options that for most basic gulp tasks you can safely omit.

The dest method receives the directory name as its first parameter and stores all files from the incoming stream in that directory. Then, the stream is pushed further into the pipeline so you can continue piping and processing.

Typically when writing tasks that return streams, you’ll start with gulp.src, then chain several .pipe() steps, and then pipe the results into the gulp.dest stream. The heart of gulp tasks are those steps that happen between gulp.src and gulp.dest. However, while they make gulp as powerful as it is, these steps themselves are not part of gulp, they are actually plugins – external tools, often written by other people, sometimes even you, that perform various transformations on the stream they receive, and then pass the transformed stream further down the pipeline. On some abstract level, this could be a stream workflow for building the JavaScript script file for a pre-2018 control add-in:

function controlAddIn() {
    return gulp.src("src/**/*.js")
        .pipe(concat("script.js"))
        .pipe(babel())
        .pipe(minify())
        .pipe(dest("resource/Script"));
}

These functions that you see here (concatenate, babel, minify) they are not part of gulp per se, but they receive a stream of files, perform certain task on that stream, and then push the results down the pipeline. In my next post, I’ll talk about various kinds of plugins you can use to take your gulp tasks to the next level.

Vjeko

Vjeko has been writing code for living since 1995, and he has shared his knowledge and experience in presentations, articles, blogs, and elsewhere since 2002. Hopelessly curious, passionate about technology, avid language learner no matter human or computer.

This Post Has 4 Comments

Leave a Reply