nicosommi

javascript developer

posts

slides

about

A common pitfall when using “global” (not really) CLI’s in your node.js project (like gulp)

March 30, 2016
7 min read

“Little things can make a big difference.”

“Little things can make a big difference.” I’m sure you heard that phrase a lot. Well there are cases in which that is true. And this is, at my opinion, one of those cases. Some people know about this detail I’m going to talk about, but most people don’t.

Why? Almost everyone, including me until recently, when they decide to use a tool, like gulp, on one of their node.js projects, they creates a gulp file on the project root with some tasks like “test” on it for example, and then run something like

gulp test

Right?

Well. I understand you if that’s your case, because they tell you to do so in the official documentation. I did the same some time ago. But, in most cases, that’s a bad thing to do. I will say that you should stop doing that ASAP. Not the gulpfile, that’s ok, but I have what I think it is a solid set of arguments against using gulp as a top level task runner (I mean installed globally). Let me explain you why.

Firstly, I want my node.js project to be cloned and executed right away. Period. I do not want the container / user / developer / anybody to install something globally on their environments besides of the natural requirements for that technology. Because is just unrelated to my node.js project environment. It’s not a global dependency, it is a local dependency. The flow should be something more like: git clone, npm install, npm test. And that’s it.

Let’s imagine this situation for a minute. I’m making public modules relying on the global gulp. On the other hand, you’re a developer and you’re new to node.js… then you start with some project and you decide to clone one of my modules. Now, suddenly, if you want to run the test or maybe do some other task like contributing to my project, you need to install some global tool called, in this example, gulp. So now you need to read lot’s of docs to know what it is and how it works. All right, I would say that coupling gulp learning curve to my node.js modules is not intuitive and it’s also unfair for those developers who don’t know about our custom tool preference or they just don’t want to learn it. So by doing this, you’re not just creating a dependency for your projects in your environments but also for the users/potential contributors of your module if it’s a public module, or the developers in your team if it’s not.

I would say that coupling gulp learning curve to my node.js modules is not intuitive and it’s also unfair for those developers who don’t know about our custom tool preference or they just don’t want to learn it. So by doing this, you’re not just creating a dependency for your projects in your environments but also for the users/potential contributors of your module if it’s a public module, or the developers in your team if it’s not.

Wait, this is not just some hedonistic crap. And it’s not just me believing it’s better. That thing can hurt your open source strategy and it can make you lose time with your team.

And there are even more real conflicts with this… specifically with module versions. If you don’t believe me, just start playing with babel versions 5, 6 along with gulp versions 2 and 3 in two or three different projects, and you will face a lot of problems. Because you’re sharing dependency versions among projects by using a global CLI. Gulp (like almost any other popular CLI in node.js) is not really a global CLI, is a dependency of each one of your projects. So on each case you really need to ask to yourself “is this CLI a real global dependency or is related to my projects individually?”. Because if you decide this wrong, this may force you to update your old projects after you update one, or switch constantly global CLI versions, and this task can be very annoying, making you lose time and money.

You may think, well anyway I just have a few projects and if I refresh one I can just refresh all of them. Well if you grow you will have troubles this is a side effect on the team plan. It’s creating unexpected efforts. And it may be not so easy to figure out which versions are failing.

**Gulp (like almost any other popular CLI in node.js) is not really a global CLI, is a dependency of each one of your projects. So on each case you really need to ask to yourself “is this CLI a real global dependency or is related to my projects individually?” Because if you decide this wrong, this may force you to update your old projects after you update one, or switch constantly global CLI versions, and this task can be very annoying, **\making you lose time and money_**.

**_

Even if gulp get a really amazing development process in which they fully stick to backwards compatibility, this is not a recommended practice at all, mainly because you are creating a node.js project, not a gulp project. And it does not depend on one framework as I told you with the babel/gulp letal combination. I remember that day. We literally had to refresh all our modules because they suddenly started to fail in the continuous integration server when the environment changed.

I remember that day. We literally had to refresh all our modules because they suddenly started to fail in the continuous integration server when the environment changed.

Introduction to the “trivial” solution

Node.js comes with the npm binary built in. Npm supports, via package.json, running scripts in different stages or phases and also custom scripts. Also, some packages like gulp have a binary link for which npm creates a symbolic link automatically on a project scoped bin folder and npm run includes those links in the PATH automatically for the scripts in the package.json.

npm creates a symbolic link automatically on a project scoped bin folder and npm run includes those links in the PATH automatically for the scripts in the package.json.

You can see a reference here https://docs.npmjs.com/cli/run-script

So the point is to use the local gulp binary link in the package.json as a shortcut for executing your internal gulp tasks. And, luckily, that is transparent to us.

So the point is to use the local gulp binary link in the package.json as a shortcut for executing your internal gulp tasks

How to do this?

Well basically, let’s say you want to replace your gulp test command so you don’t need a global gulp… well ok, then simply add this to your scripts section of your package.json

...(content on your package.json)...
"scripts": {
"test": "gulp test"
}
...(more content on your package.json)...

And that’s it! With that line now you just need a node js regular environment with nothing installed globally on it, like this

npm test

Trivial? Well not so fast. This is the small detail. If you read the npm documentation, maybe you are doing this already. But according to my experience and what I see out there, some people do this but they don’t know what happens behind the scenes and most people just use the global gulp. This npm test command is not executing the global gulp. The better way to experiment this is just uninstalling the global gulp. And please do that ASAP! You just need to install gulp (or the CLI you want to make local) locally in your project before you execute the script through npm run… remember that. So this is picking the link from the PATH that points to the node_modules version of gulp of your project.

npm uninstall -g gulp

**This npm test command is not executing the global gulp. The better way to experiment this is just uninstalling the global gulp. And please do that ASAP! You just need to install gulp (or the CLI you want to make local) locally

**

**Sugar for complex cases

**

As I said earlier, you can use custom scripts (meaning any name you want) on the scripts element on your package.json. If that’s your case, after you added it on your package.json, you need can run that custom npm script with this command

npm run gulp yourcustomtask

And here, an elegant ultimate approach that let you execute any gulp task you may have with the bin symbolic link fix with a command like npm run gulp anytask, check it out:

...(content on your package.json)...
"scripts": {
"test": "gulp test",
"gulp": "gulp"
}
...(more content on your package.json)...

Other examples

And as I mentioned I’m not talking about just gulp, eslint is another good example of this concept but in a slightly different way, you can configure your IDE to use some global eslint set of rules, but every project may have different rules, and this is a real conflict too because one maybe is in es5 and the other one in ECMAScript 2015, and there lots of examples. Grunt also applies, you can stop using the global grunt by following this steps too (installing grunt-cli locally instead). Want more? Mocha, some Babel versions. And the list is long. Of course, they are creating a module, they want it to be easy so more developers adopt it. And it’s true, it works, they get more engagement on their modules. But at some point we need to know where to stop and grow from our experience.

Conclusion

So in conclusion, I think every project should be self sufficient, self configured, and this approach tries to tackle that. If you create a node.js project you need to stick to the node.js interpreter as a requirement. Nothing global except for that container platform itself which is a natural requirement for your scripts. In this way, if some day another task runner came out and kills gulp, or any pseudo global CLI, you can smoothly switch to the new one. Also you don’t couple the learning curve from the CLI to your module user’s, and you don’t share CLI dependencies between your different projects you may have, letting you have different project with different versions without forcing you to refresh them all.

 

Please any comment, suggestion, typo, send it to nicosommi@gmail.com

Thanks for reading.

nico
Copyright by nicosommi 🥑