Gulp Is a Power Tool; Wield It With Care

These days, it’s quite common to see front-end projects using Gulp to run a variety of repetitive tasks. From compiling and optimizing our stylesheets, JavaScript, and images, to starting up local servers and automatic browser refreshes, Gulp has become a go-to power tool at the center of your average developer experience. And rightly so! Its code-over-configuration paradigm gives developers the control they need to design bespoke workflows to solve everyday problems, all while maintaining an easy to follow “Gulp Way” of doing things. Gulp strikes an elegant balance between ease of use and customizability.

Given its popularity, it’s not surprising to see that many projects require the gulp command line tool to be installed globally on the developer’s machine. This is more common when working with a team, of course, but many open source tools also do this. Indeed, you need gulp installed globally in order to run any gulp task directly from the command line like gulp build or gulp dev. But there is a better, less invasive way to enjoy all of Gulp’s benefits and even avoid some of its pitfalls.

Why not just install it globally?

You might be telling yourself, “It’s a common enough tool. Developers should expect to have common tools readily available.” There are several counter arguments to this:

  • It makes your project brittle. There is no guarantee that the version of Gulp that a developer has installed is the same version required by our project. This can be especially problematic if the version mismatch involves major versions (i.e. breaking changes). See the breaking changes in Gulp v4 for a good example of this.
  • It can put your project at odds with other projects. Other projects on a developer’s machine may require different versions of Gulp to be installed globally. Is a developer supposed to install a different version of Gulp every time she switches between projects? Nobody wants that.
  • It increases the number of steps necessary to get started on your project (i.e. friction). Not only does someone need to run npm install, they also now need to run npm install -g gulp (potentially also having to install a specific version, like npm install -g gulp@3).
  • It’s invasive. Expecting developers to clutter their global scope with our choice of task runner is a bit rude, if you think about it.

npx to the rescue? Eh, not quite.

While the recently-introduced npx utility, which allows you to run CLI tools without installing them permanently in your global scope, could be used to help mitigate some of the issues pointed out above, it introduces some verbosity, especially if you require an older version of Gulp. For example:

npx gulp@3.9.1 [task_name]

That’s for every Gulp task you ever need to run for a given project. Frankly, that’s gonna get old pretty soon. This also requires npx to be installed on the developer’s machine, which again is a bit of a presumption. I’d argue that if your project relies on Gulp pre-v4, then there’s a good chance you and your collaborators are still using a version of npm pre-v5.2 (which introduced npx). And while, sure, you’ll all probably upgrade eventually, why force it for the sake of a task runner, when there are more adequate solutions available?

But maybe your project relies on the latest version of Gulp and npx gulp [task_name] (without a version number, so it grabs the latest published version) doesn’t sound so bad. That sounds reasonable right now, but what if Gulp 5 is published tomorrow and it introduces changes that break your workflow? Hell, what if the next minor version introduces a bug that breaks your workflow? In both of these scenarios, you’ll either find yourself forced to make your gulpfile.js compliant in very short order (and potentially with little or no notice), or inform developers that they must now use npx gulp@[version] [task_name] until further notice.

Any which way you cut it, using npx like this is akin to carrying around a ticking time bomb.

A better way: Gulp via npm scripts

The solution is quite simple: using good ol’ npm scripts. Say you have a Gulp task called dev. In your project’s package.json file, you’d define a dev task in the scripts object:

"scripts": {
"dev": "gulp dev"

Then, in order to run Gulp’s dev task, all you need to do is run npm run dev. The beauty of this is that it uses the version of gulp installed locally for your project. npm has this really useful feature that makes any binaries from your locally installed dependencies (found inside ./node_modules/.bin) available in its execution environment, so in this case it will use the gulp binary installed at ./node_modules/.bin/gulp without us having to include the whole path in our scripts.

But what if your gulp task takes arguments? Say maybe your dev task can take a --no-sync option to turn off BrowserSync for certain scenarios. Luckily, npm can pass arguments to your scripts, like so:

npm run dev -- --no-sync

Even better, you could just define a new npm script that calls your dev script and passes the appropriate flags to it:

"scripts": {
"dev": "gulp dev",
"dev:nosync": "npm run dev -- --no-sync"

You can then invoke this new script from the command line with npm run dev:nosync.

Isn’t that easy? Using npm scripts effectively removes all of the issues associated with using globally installed Gulp:

  • It makes our project resilient by using precisely the same version of Gulp that’s required in our package-lock.json file on every machine.
  • It keeps our project isolated from others. The versions of Gulp required by the other projects on a developer’s machine can have absolutely no effect on our project, even if they require Gulp to be installed globally.
  • It keeps setup and installation simple. npm install is all our devs need to get up and running.
  • It’s respectful. No more unwarranted expectations regarding what tools should be installed on a collaborator’s global scope. How very classy of us!

Using npm scripts means we’re relying on a tool that must already exist on a developer’s machine in order to work on our project, instead of adding to that list of “absolute must-haves.” What’s more, we’re providing a more cohesive developer experience by providing a consistent set of commands that always begin with npm run. We gain reliability, consistency, and developer happiness, all with minimal setup. I call that a win!