I recently wanted to add colored output to a terminal/command line program. I checked some other project that was outputting color and saw they were using a library called chalk.
All else being equal I prefer smaller libraries to larger ones and I prefer to glue libraries together rather than take a library that tries to combine them for me. So, looking around I found chalk, colors, and ansi-colors. All popular libraries to provide colors in the terminal.
chalk is by far the largest with 5 dependencies totaling 3600 lines of code.
Things it combines
It combines checking whether or not the your output stream supports colors. Because of this it has to add way to tell it don't check for me because I'll do the checking myself
It peeks into your application's command line arguments magically looking for
--color
or--no-color
so without modifying your app or documenting what arguments are valid it will look at these arguments. If your app uses those arguments for something else you lose.It combines all the named colors from HTML even though they are of questionable usefulness in a terminal.
It includes 800 lines of code for color conversions so you can use rgb or hsl or lab or cmyk etc.
Next up is colors. It's about 1500 lines of code.
It hacks the string prototype. The author seems to think this is not an issue.
It has a theme generator which works something like this
colors.setTheme({
cool: 'green',
cold: 'blue',
hot: 'red',
});
And you can now do
colors.hot('the sun');
Like chalk it also spies on your command line arguments.
Next up is ansi-color. It's about 900 lines of code. It claims to be a clone of colors without the excess parts. No auto detecting support. No spying on your command line. It does include the theme function if only to try to match colors API.
Why all these hacks and integrations?
Themes
Starting with themes. chalk gets this one correct. They don't do anything. They just show you that it's trivial to do it yourself.
const theme = {
cool: chalk.green,
cold: chalk.blue,
hot: chalk.red,
};
console.log(theme.hot('on fire'));
Why add a function setTheme
just to do that? What happens if I go
colors.theme({
red: 'green',
green: 'red',
});
Yes you'd never do that but an API shouldn't be designed to fail. What was the point of cluttering this code with this feature when it's so trivial to do yourself?
It gets worse though because a new user who sees console.log(colors.hot(someMsg))
will be effectively be taught that colors.hot
is an official API of colors
. Then'll then copy that to some other project and learn that in fact no, it's an app specific hack. If they had used more direct way it arguably becomes clear. I've had to help hundreds of users on stackoverflow where some example they saw had monkey patched on a non-standard feature to some object and then when they tried to use it in their own code they got an error and didn't understand why because it looked like part of the official API but wasn't.
Color Names
It would arguably be better to just have them as separate libraries. Let's assume the color libraries have a function rgb
that takes an array of 3 values. Then you can do this:
const pencil = require('pencil');
const webColors = require('color-name');
pencil.rgb(webColors.burlywood)('some string');
vs
const chalk = require('chalk');
chalk.keyword('burlywood')('some-string');
In exchange for breaking the dependency you gain the ability to take the newest color set anytime color-name is updated rather than have to wait for chalk to update its deps. You also don't have 150 lines of unused JavaScript in your code if you're not using the feature which you weren't.
Color Conversion
As above the same is true of color conversions
const pencil = require('pencil');
const hsl = require('color-convert').rgb.hsl;
pencil.rgb(hsl(30, 100, 50))('some-string');
vs
const chalk = require('chalk');
chalk.hsl(30, 100, 50)('some-string');
Breaking the dependency 1500 lines are removed from the library that you
probably weren't using anyway. You can update the conversion library if there are bugs or new features you want. You can also use other conversions and they won't have a different coding style.
Command Line hacks
As mentioned above chalk looks at your command line behind the scenes. I don't know how to even describe how horrible that is.
A library peeking at your command line behind the scenes seems like a really bad idea. To do this not only is it looking at your command line it's including another library to parse your command line. It has no idea how your command line works. Maybe you're shelling to another program and you have a —-
to separate arguments to your program from arguments meant for the program you spawn like Electron and npm. How would chalk know this? To fix this you have to hack around chalk using environment variables. But of course if the program you're shelling to also uses chalk it will inherit the environment variables requiring yet more workarounds. It's just simply a bad idea.
Like the other examples, if your program takes command line arguments it's literally going to be 2 lines to do this yourself. One line to add --color
to your list of arguments and one line to use it to configure the color library. Bonus, your command line argument is now documented for your users instead of being some hidden secret.
Detecting a Color Terminal
This is another one where the added dependency only detracts, not adds.
We could just do this:
const colorSupport = require('color-support');
const pencil = require('pencil');
pencil.enabled = colorSupport.hasBasic;
Was that so hard? Instead it chalk tries to guess on its own. There are plenty of situations where it will guess wrong which is why making the user add 2 lines of code is arguably a better design. Only they know when it's appropriate to auto detect. (PS: you may want to detect stderr separate from stdout via something like colorSupport({stream: process.stderr}).hasBasic
).
Issues with Dependencies
There are more issues with dependencies than just aesthetics and bloat though.
Dependencies = Less Flexible
The library has chosen specific solutions. If you need different solutions you now have to work around the hard coded ones
Dependencies = More Risk
Every dependency adds risks.
- Risk there will be a security vulnerability
- Risk a dependency will be abandoned
- Risk the library you want to use will depend on a old outdated version of one of its dependencies
- Risk a malicious actor will compromise one of the dependencies
- Risk by expanding the number of people you have to trust.
You need to trust every contributor of every dependencies. A library with 5 dependencies probably has between 5 and 25 contributors. Assuming the high end that's 25 people you're trusting to always do the right thing each time the library is updated. Maybe they got angry today and decided to take their ball home or burn the world. Maybe they got offered $$$$$$$ to help hack someone and needed the money for their sick mom. Maybe they introduced a bug by accident or wrote a vulnerability by accident. Each dependency you add adds a larger surface area for these issues.
Dependencies = More Work for You
Every dependency a library uses is one more you have to deal with. Library A gets discontinued. Library B has a security bug. Library C has a data leak. Library D doesn't run in the newest version of node, etc…
If the library you were using didn't depend on A, B, C, and D all of those issues disappear. Less work for you. Less things to monitor. Less notifications of issues.
Lower your Dependencies
I picked on chalk and colors here because they're perfect examples of a poor tradeoffs. Their dependencies take at most 2 lines of code to provide the same functionality with out the dependencies so including them did nothing but add all the issues and risks listed above.
It made more work for every user of chalk since they have to deal with the issues above. It even made more work for the developers of chalk who have to keep the dependencies up to date.
For chalk, just like they have a small blurb in their readme on how to implement themes they could have just as easily shown how to do all the other things without the dependencies using just 2 lines of code!
I'm not saying you should never have dependencies. The point is you should evaluate if they are really needed. In the case of chalk it's abundantly clear they were not. If you're adding a library to npm please reduce your dependencies. If it only takes 1 to 3 lines to reproduce the feature without the dependency then just document what to do instead of adding a dep. Your library will be more flexible. You'll expose your users to less risks. You'll make less work for yourself because you won't have to keep updating your deps. You'll make less work for your users because they won't have to keep updating your library just to get new deps.
Less dependencies = Everyone wins!
Top comments (0)