lukeed

The tools that did not win

Name

tools-that-lost — the tools that did not win

Synopsis

Taskr, pwa-cli, and freshie were each my answer to a big question in JavaScript tooling, and each one lost. Here is what they taught me.

Filed

· 1,337 words · 11 min

Description

Most posts about old projects are about the ones that worked. This one is about three that did not.

Taskr, pwa-cli, and freshie were each my answer to a big question at the time. How should you run build tasks? How should you start a web app? How should a framework talk to a server? Other tools answered better, or louder, or later and with more people. Between them, they have almost 6,000 GitHub stars and almost no users today.

I do not think they were wasted. Each one taught me something the winners also learned, and most of my small packages started inside one of them.

#Taskr: generators everywhere

Taskr did not start as mine. It started as Fly, a task runner by Jorge Bucaran. The first commit is from June 2015, and its message is "It's a fly!". I was one of its early contributors. When Jorge moved on to other projects, he gave the project to me, and in June 2017 I renamed it to Taskr.

The pitch was in the README: "A fast, concurrency-focused task runner." It was like Gulp, but every task was a generator function, and you could yield anything:

module.exports = {
  *scripts(task) {
    yield task.source("src/*.js").babel().target("build/js");
  },
  *build(task) {
    yield task.parallel(["lint", "scripts", "styles"]);
  },
};

It was fast, and the plugin list grew big: Babel, Sass, PostCSS, TypeScript, Prettier, Jest, gzip, and more.

Why it lost. Two things happened at the same time. First, async/await arrived, and the generator syntax that made Taskr feel modern started to look odd. Second, and more important, people stopped writing build pipelines. Webpack, and later Vite, took over the work of turning sources into a bundle. What was left was small enough for npm run. A task runner is useful when you have many tasks. Most projects stopped having many tasks.

What it taught me. Taskr was my first experience owning a project that other people depended on, and inheriting a codebase I did not design. It taught me that a plugin system is a promise you make to every plugin author, and that each plugin is a package you have to maintain when the core changes. Twenty-three plugin packages is a lot of promises.

It also taught me that "concurrency" is not a feature users ask for. Speed is. Concurrency is one way to get there, and the user should not need to know it is happening.

#pwa-cli: one command for every framework

PWA came next. The idea: one CLI that scaffolds, builds, and exports a Progressive Web App, whatever framework you use.

From the README:

  • Framework agnostic, with official presets for Preact, React, Vue, and Svelte.
  • Plug 'n play: presets and plugins apply themselves when you install them.
  • Babel, Bublé, Browserslist, TypeScript, PostCSS, ESLint, Prettier, and service workers out of the box.
  • Start with a perfect Lighthouse score.
  • Export routes as pre-rendered HTML.

The "plug 'n play" part was the idea I liked most. You did not edit a config file to add TypeScript. You installed @pwa/plugin-typescript, and PWA found it in your package.json and wired it in. The core was a set of webpack defaults, and every preset and plugin changed them.

Why it lost. The core was webpack 4 and about twenty loaders and plugins. Every one of those had its own major versions, and every major version was a chance for one of my presets to break. The README kept a "WORK IN PROGRESS" banner the whole time, and the last real commits are from August 2019.

Then the platform moved. Native ES modules in the browser made the dev server cheap. Vite showed up in 2020 with a dev server that did not bundle at all, and a plugin system based on Rollup's. Framework teams started to ship their own CLIs. A tool that sat between you and four frameworks was now competing with each of those frameworks' own answer.

What it taught me. Zero config is a promise about defaults, not about the absence of config. The good part of PWA was that the defaults were opinionated and the escape hatch was a function. Vite does the same. The bad part was that I was wrapping a large tool I did not control, so my opinions always sat one breaking change away from not working.

It also left small packages behind. @pwa/webpack-assets shipped as its own module, and the webpack plugins I wrote around that time outlived the CLI.

#freshie: pluggable frontends and backends

Freshie started in September 2020. The README describes it as "a fresh take on building universal applications with support for pluggable frontends and backends."

Two axes:

  • UI: @freshie/ui.preact, @freshie/ui.svelte, @freshie/ui.vue.
  • SSR: @freshie/ssr.node and @freshie/ssr.worker.

The worker target is the interesting one. In 2020, rendering on Cloudflare Workers was rare. Freshie let you pick a UI library and a server target independently, and it built a client bundle and a server bundle for that pair, with Rollup and esbuild.

It also had a routes directory, and the framework read the file tree to build the route table. Yes, I have since written that file system routers are a mistake. Freshie is part of how I learned that.

Why it lost. It was never finished. The README still says "Functional, but very incomplete" and "There will be more here, I promise". The last real work was in late 2021. In the same period, SvelteKit, Astro, Remix, and others shipped the same idea, with adapters for Node, Workers, and everything else, and with full teams behind them.

What it taught me. The "pluggable backend" idea was right. Today every serious meta-framework has adapters, and the Workers target is normal. But the idea was not enough. A framework needs docs, examples, and years of people filing bugs, and I had a README that promised more later.

It also ran next to worktop, which I started in April 2020, five months before freshie. Both were about the same platform. Worktop handled requests inside a Worker. Freshie tried to render a whole app there, and its ssr.worker package did its own routing with regexparam.

#What the winners shipped

Put the three side by side and the pattern is plain.

  • Taskr wanted builds to be fast by default and composable without streams. Modern bundlers and npm run give you that.
  • PWA wanted install-to-enable plugins and one CLI for every framework. Vite has framework-agnostic plugins and create-vite templates for every framework.
  • Freshie wanted to pick a UI and a server target separately. Adapters do exactly that.

I do not say this to claim credit. Lots of people had these ideas at the same time, and the people who shipped them did the hard part. I say it because it changes how I see these projects. They were not wrong. They were early, or alone, or built on a base that was about to move.

#The parts that lived

The thing I am happiest about is what came out of them.

Freshie's own dependencies tell the story: escalade, kleur, klona, mri, navaid, totalist, route-sort, rollup-route-manifest. Most of those are mine, and most of them were written because a larger project needed one small thing done well. The large projects stopped. The small parts went on to billions of installs.

That is the pattern I trust now. When I build something big, I build it out of pieces that would survive on their own. If the big thing loses, the pieces do not have to.

#How I decide what to archive

The honest answer: I mostly do not.

None of the three repos is archived on GitHub. Taskr still gets about 94,000 downloads a month, which means somebody's build still runs on it. Archiving would make the repo read-only and send a clear signal, and I think that signal is right. I have not done it.

A project that lost is not a failure if you keep what it taught you. I would rather have written all three and lost than never tried. I just want to be clearer, now, about when I am done.

See also

open-source(7), build-tools(7), history(7), javascript(7), lukeed(1)