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 email@example.com [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
"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:
"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.jsonfile 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 installis 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!