Exporting tasks from gulpfile.js

In my last post I covered the very basics of getting started with gulp in VS Code. It was all about getting gulp up and running under the hood of VS Code, and about writing your very first gulp task. It didn’t do much, but it showed you what gulpfile.js is good for and how to export tasks from it, that you can later use from VS Code.

However, I mentioned that there is much more to exporting tasks than it seems on the surface, and I promised to blog more about it. So here we are. Tasks.

Let’s take a look at our gulpfile.js where we left it last time:

const gulp = require("gulp");
function helloWorld(callback) {
    console.log("Hello, World!");
    callback();
}
gulp.task(helloWorld);

Strictly speaking, what you did here was not exporting a task. What you did was registering a task. We’ll see about exporting tasks in a short while.

This is a very simple way of exporting tasks. You call gulp.task, pass a reference to a function, and there you go. When you do this, gulp will simply take the name of the function and use it as the name of the task. However, you can use a custom name of the task by passing it as the first parameter to your task method, like this:

gulp.task("hello", helloWorld);

If you check your gulp tasks with gulp –tasks now, you’ll see this:

Since this is JavaScript, where functions are first-class, you can pass an anonymous function as the function parameter, like this:

gulp.task("hello", function(callback) {
    console.log("Hello, World!");
    callback();
});

… or if you want a more modern flavor to it, use the arrow syntax (sorry for this, but my WordPress doesn’t allow me to show => inside code blocks, so any time you see => it means =>):

gulp.task("hello", callback => {
    console.log("Hello, World!");
    callback();
});

However, when passing anonymous functions to gulp.task, you must specify the name. The following will be rejected by gulp:

gulp.task(callback => {
    console.log("Hello, World!");
    callback();
});

The problem here is that all gulp tasks must have names, and this function is, well, anonymous. Not a good match for gulp. Another problem is that this makes your entire gulpfile.js unusable. Even if you have other correctly exported tasks, having a single task without a name prevents you from invoking any of the tasks. Let’s take a look:

gulp.task("hello", callback => {
    console.log("Hello, World!");
    callback();
});
gulp.task(callback => {
    console.log("Hello, World!");
    callback();
});

The first task is correctly exported with a name, but the other task has no name. Trying to invoke the first task fails because your entire gulpfile.js must be consistent:

There is another interesting JavaScript feature of which gulp takes advantage. In JavaScript functions and objects support expando properties. When you add certain properties to functions configured as gulp tasks, gulp can use those properties as metadata.

For example, you can do this:

const gulp = require("gulp");
function helloWorld(callback) {
    console.log("Hello, World!");
    callback();
}
helloWorld.description = "Prints 'Hello, World!' in the console.";
gulp.task(helloWorld);

Running gulp –tasks now gives you some more info:

There is more metadata you can add to your gulp tasks to describe them, but for now they are a tiny little bit too advanced for where we are. We can talk about them later.

However, as I said, this was registering tasks. There is another way to make tasks available to the gulp runtime: exporting them. What does exporting mean, and what’s the difference between registering and exporting? It has to do with Node.js.

The entire Node.js infrastructure relies on modules. A module is an independent piece of functionality that you can plug in into your Node.js runtime to provide specific functionality for you. For example, gulp itself is a Node.js module that provides all this task automation functionality to your Node.js environment (remember: VS Code is built on Node.js). For a module to be useful to Node.js, it must export some functionality. To export functionality means to provide public methods that consumers of that module can use. Just like making a “global” function inside a gulpfile.js doesn’t automatically make that function a task, similarly including a “global” function inside a Node.js module doesn’t make it accessible for anyone outside. To provide a function to the outside world you must export it.

Exporting something from a Node.js module is achieved in one of the two ways: through the module.exports object, or through exports variable, which is nothing but a shortcut to module.exports object.

For example, if you are writing a Node.js module, and it has a helloWorld function that you want to export, you could simply do this:

module.exports.helloWord = function (callback) {
    console.log("Hello, World!");
    callback();
};

When you do this, you are allowing anyone who wants to consume your module to invoke the helloWorld method of your module.

But why do you care? You are writing a gulpfile.js, not a Node.js module, aren’t you? Well, not quite. As a matter of fact, strictly speaking, you are writing a Node.js module. Every gulpfile.js is a Node.js module, and both gulp and Node.js treat it that way. That’s why this last code example works just fine inside your gulpfile.js. Replace your entire gulpfile.js with the example above, and gulp –task will still happily show you your task:

Don’t worry, you can still attach your description metadata, like this:

module.exports.helloWorld.description = "Prints 'Hello, World!' in the console.";

Sometimes, you may find that some gulp files export modules not using module.exports but simply exports, like this:

exports.helloWorld = function (callback) {
    console.log("Hello, World!");
    callback();
};
exports.helloWorld.description = "Prints 'Hello, World!' in the console.";

That will work, too, because both module.exports and exports point to the same object. However, you should stick with the fully qualified syntax, simply because it’s more robust. Let’s see why.

It has to do with how JavaScript works, or better yet, it has to do with how you would expect a decent programming language to work. You should first ask yourself a very simple question: where do modules and exports variables come from in the first place? Let me tell you one thing: they are not keywords or reserved words, and there is no magic and no gimmick controlling what they are and what they do.

Imagine that your entire gulpfile.js operates in an isolated function scope, and that function receives module and exports as two separate parameters, and it’s invoked like this:

var module = {
    exports: {}
};
(function(exports, module) {
    // Here comes your module code! 
})(module.exports, module);

Well, good news is, you don’t really have to imagine this. This is, simplified, exactly what Node.js does with every module. It wraps the module in a module wrapper that provides all the “built-in” functionality to your modules.

Now, understanding that module.exports points to the same instance of an empty object {} that exports points to, you understand why your code above worked exactly the same way regardless of whether you used module.exports or simply exports.

But, why aren’t they equally good? Because you could use object literal syntax when exporting a module, like this:

module.exports = {
    helloWorld: function (callback) {
        console.log("Hello, World!");
        callback();
    }
};

This is especially useful if you want to export more than one function:

// Example 1:
module.exports = {
    helloWorld: function (callback) {
        console.log("Hello, World!");
        callback();
    },
    helloMoon: function(callback) {
        console.log("Hello, Moon!");
        callback();
    }
};

This nicely exports two gulp tasks:

However, you may think that this works the same:

// Example 2:
exports = {
    helloWorld: function (callback) {
        console.log("Hello, World!");
        callback();
    },
    helloMoon: function(callback) {
        console.log("Hello, Moon!");
        callback();
    }
};

… but it does not:

The thing is, when evaluating tasks, gulp checks the exports property of the module object. Whatever gulp runtime encounters when evaluating module.exports at runtime will become your gulp tasks. This is exactly what happens in Example 1 above. However, if you assign something directly to exports, like in Example 2, you are simply replacing the reference inside the exports variable with another object. When you start off, both exports and module.exports point to the same object. Assigning something directly to exports variable, you replace its previous reference with a new one.

It might sound a bit scary to a C/AL or AL developer with no experience with object-oriented languages or reference types, and if this does sound scary, then simply stick with modules.exports syntax and you are safe.

That’s more or less it, that’s how you export modules from your gulpfile.js.

Now, is there any difference between registering a task (gulp.task) and exporting a task (module.exports)? Honestly, not really. Both are valid ways, and both work, and you can even combine both in the same gulpfile.js. Still, gulp’s own documentation says that it’s better to use module.exports and that should be all you care about. Maybe gulp.task gets deprecated in the future, maybe gulp folks knows something that they are not saying out loud, I don’t know. I put reasonable effort in trying to figure out why exactly gulp.task is not a preferred way anymore but I couldn’t. If I get a chance once I may blog about a more advanced scenario of structuring your gulp tasks in individual files, where using gulp.task becomes a preferred way of handling your gulp tasks, but let’s leave it for another time.

My next blog post will be specifically about gulp tasks and how to write them. See you around!

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 One Comment

Leave a Reply