JavaScript Colors and the Corruption of Buy vs. Build

In an apparent fit of pique, the maintainer of the popular JavaScript “colors” package has intentionally released a broken update that causes any program importing the package to go into an infinite loop. (Colors is a tool that inserts ANSI color codes into a string, making it easy to output colored text.)

This is part of a larger conversation in the wake of the Log4Shell exploit around open source maintainers’ high maintenance burden and low compensation.

What interests me about colors, though, is how widely used it is. Twenty-two million weekly downloads, according to npm. In my mind, “colors” represents a corruption of the “buy vs. build” tradeoff. It’s no longer “buy vs. build,” but “only build when you can’t find any other alternative.” “Not invented here” is an insult. And so people use third-party code without thinking it through.

As it happens, just last month I needed a JavaScript library for colorizing strings, and “colors” was one of the packages I looked at. I immediately decided against it, though. Whenever I evaluate a new dependency, I’m looking at two things:

  1. How much time will this save me now?
  2. How much time will this cost me later?

Looking at the “colors” documentation and code didn’t fill me with confidence. This is a library that loves fun. Not only will it color-code your strings, it will make rainbows. It will substitute Unicode characters. It will “zalgo-ify” your text.

Let’s be honest. The long tail of open source libraries is filled with crap. I get the impression that the majority of them are created by early-twenties college students and fresh grads with lots of time on their hands and an eagerness to make a name for themselves. They’re filled with bad coding practices.

So where others see “fun,” I see risk. That feeling was clinched when I saw that, by default, “colors” monkeypatches the String global. No thank you.

I looked into other options, but they didn’t impress either. “How hard could this be?” I asked myself. One Stack Overflow search later, I had my answer: not hard at all.

  1. How much time will “colors” save me now? Probably an hour or two.
  2. How much time will it cost me later? The code has a lot of red flags. It’s high risk, and could easily cost me more time than it saves.

So I built it myself. It took a few hours, according to my git logs, and less than a hundred lines of code.

It’s rare that my hunches are vindicated so quickly, but less than a month later, “colors” imploded. Those few hours have already been saved.

The moral of the story? Don’t let the “not invented here” epithet scare you. With attention to tests and good design, you can create code that is much simpler and easier to maintain than third-party code. In general, if something will take me less than a day a two of work, I’ll build it myself. Third-party code is so much harder to tweak to match my needs, and quality tends to be worse, too. Even when it’s not, third-party code is an ongoing tax on development. It’s not custom-tailored to my needs, so it takes additional time to understand and integrate, and updates and API changes force continual maintenance.

The pendulum has swung too far in favor of “buy” over “build.” It’s time it swung back.

In case you’re curious, here’s the code I wrote. You’re welcome to use this yourself, on the condition that you don’t ask me for support.

Usage:

const { red } = require("./colors");    // import other colors as needed
console.log(red("red text"));
console.log(red.underline("red text with underline"));

_colors_test.js:

// Copyright 2021 Titanium I.T. LLC.
// This code is licensed under the terms of the MIT license (https://mit-license.org/)
// under the condition that you do not request support without a paid support contract.
"use strict";

const describe = require("tests/describe");
const assert = require("tests/assert");
const colors = require("./colors");

module.exports = describe("Colors", ({ describe, it }) => {

  const { red } = colors;    // see production code for other supported colors

  it("color-codes text", () => {
    assert.equal(red("text"), "\u001b[31mtext\u001b[0m");
  });

  it("has styling", () => {
    // note that support for styles depends on terminal emulator

    assert.equal(red.bold("text"), "\u001b[1;31mtext\u001b[0m", "bold");
    assert.equal(red.dim("text"), "\u001b[2;31mtext\u001b[0m", "dim");
    assert.equal(red.underline("text"), "\u001b[4;31mtext\u001b[0m", "underline");
    assert.equal(red.blink("text"), "\u001b[5;31mtext\u001b[0m", "blink");
    assert.equal(red.inverse("text"), "\u001b[7;31mtext\u001b[0m", "inverse");
  });

  it("can chain styles", () => {
    assert.equal(red.bold.underline("text"), "\u001b[1;4;31mtext\u001b[0m", "multiple styles");
    assert.equal(red.underline.bold("text"), "\u001b[4;1;31mtext\u001b[0m", "use any order");

    assert.isUndefined(red.bold.bold, "doesn't repeat styles");
    assert.isUndefined(red.bold.underline.bold, "doesn't repeat styles even recursively");
  });

});

colors.js

// Copyright Titanium I.T. LLC.
// This code is licensed under the terms of the MIT license (https://mit-license.org/)
// under the condition that you do not request support without a paid support contract.
"use strict";

const COLOR_STYLES = {
  bold: "1;",
  dim: "2;",
  underline: "4;",
  blink: "5;",
  inverse: "7;",
};

module.exports = {
  // this brute-force approach works better with IDE code completion than building the object at run-time.
  black: colorFn(30),
  red: colorFn(31),
  green: colorFn(32),
  yellow: colorFn(33),
  blue: colorFn(34),
  purple: colorFn(35),
  cyan: colorFn(36),
  white: colorFn(37),
  brightBlack: colorFn(90),
  brightRed: colorFn(91),
  brightGreen: colorFn(92),
  brightYellow: colorFn(93),
  brightBlue: colorFn(94),
  brightPurple: colorFn(95),
  brightCyan: colorFn(96),
  brightWhite: colorFn(97),
};

function colorFn(color) {
  const fn = encodeFn("", color);
  combinatorialize(fn, "", color, COLOR_STYLES);
  return fn;

  function encodeFn(style, color) {
    return (text) => {
      return `\u001b[${style}${color}m${text}\u001b[0m`;
    };
  }

  function combinatorialize(fn, baseStyle, color, styles) {
    // adds .bold, .dim, etc. to fn, and does so recursively.
    Object.keys(styles).forEach(styleKey => {
      const myStyle = baseStyle + styles[styleKey];
      fn[styleKey] = encodeFn(myStyle, color);

      const remainingStyles = { ...styles };
      delete remainingStyles[styleKey];
      combinatorialize(fn[styleKey], myStyle, color, remainingStyles);
    });
  }
}

If you liked this entry, check out my best writing and presentations, and consider subscribing to updates by email or RSS.