← back to gallery

toolchain-shift

Live npm-download race between every JS tooling incumbent and its Rust challenger.

dev-toolsjavascriptrustnpmleaderboardbiomerspacktsdownvitestoxcbun
Open product ↗

toolchain-shift

The Rust-rewrite scoreboard for JavaScript tooling. Live weekly-download leaderboard of incumbent-vs-challenger pairs in JS tooling — ESLint vs Biome, Webpack vs Rspack, tsup vs tsdown, Jest vs Vitest, and 14 more. Real public data. Daily refresh. Zero auth.

Live: https://holyai.me/toolchain-shift/

Why

In 2025–2026 every legacy JS toolchain has a Rust (or sometimes Go/Zig) challenger competing
on speed and bundle size: Biome eats ESLint+Prettier, Rspack eats Webpack, Rolldown eats
Rollup, tsdown eats tsup, Vitest+Bun eat Jest, Oxc eats SWC. Picking the wrong side of the
wave is a future migration tax. No public dashboard tracked the actual crossovers — only
blog posts and conference talks.

toolchain-shift is that dashboard. For 18 curated pairs it pulls weekly npm download counts,
computes the challenger's share of total downloads, tracks tipping-point crossings (25/50/75%),
and surfaces the week-over-week delta in percentage points.

Data sources

Every number on the site is fetched at runtime from one of these four public endpoints. No
auth, no API key required (a GITHUB_TOKEN is optional to raise the GitHub rate limit, but
the product works without one). No mocks. No seeds. No Math.random(). If a fetch fails the
row is left stale and the failure is recorded in the refresh log.

| Endpoint | Used for | Refreshed |
|---|---|---|
| https://api.npmjs.org/downloads/range/{start}:{end}/{pkg} | daily download counts (90-day window) | daily 03:30 UTC |
| https://registry.npmjs.org/{pkg} | latest version, last publish date, license, homepage | daily 03:35 UTC |
| https://api.github.com/repos/{owner}/{repo} | stars, forks, open issues, pushed_at | daily 03:40 UTC |
| https://api.github.com/repos/{owner}/{repo}/releases?per_page=30 | latest release tag, releases in last 90 days | daily 03:45 UTC |

Pair snapshots (share %, delta pp, tipping crossings) are recomputed at 04:00 UTC.
Daily download rows older than 180 days are pruned every Sunday 05:00 UTC.

Stack

Setup

npm install
npm start
# server listens on :4818, mounted at /toolchain-shift
# health: curl http://localhost:4818/toolchain-shift/health
# pairs:  curl http://localhost:4818/toolchain-shift/api/pairs

On first boot (empty DB) a background refresh fires automatically; the HTTP server is
responsive to /health immediately while the refresh runs in the background. The first full
pull takes ~3 minutes when unauthenticated to GitHub (4 calls × 36 packages, paced).

Environment

| Var | Default | Purpose |
|---|---|---|
| PORT | 4818 | HTTP port |
| BASE_PATH | /toolchain-shift | Path prefix for every route (nginx proxies upstream here) |
| DB_PATH | ./data/toolchain-shift.db | SQLite file path |
| GITHUB_TOKEN | _(unset)_ | Optional. Raises GitHub rate limit from 60/hr to 5000/hr. |
| USER_AGENT | toolchain-shift/1.0 (+...) | Sent to npm and GitHub on every fetch |
| REFRESH_AT_BOOT | true | Set to false to skip the first-boot refresh |

.env.example is provided.

HTTP API

All paths are prefixed with BASE_PATH (default /toolchain-shift). All endpoints are public.
JSON GETs are cached with Cache-Control: public, max-age=60; SVG endpoints with max-age=600.

| Method | Path | Purpose |
|---|---|---|
| GET | /health | {ok, version, pairs, packages, last_refresh, last_error} |
| GET | /api/pairs | All pairs with current snapshot. Query params: category, sort, order |
| GET | /api/pairs/:slug | One pair + both packages' metadata + 90-day download series |
| GET | /api/packages | List every tracked package |
| GET | /api/packages/:pkg | Single package metadata + 90-day downloads |
| GET | /api/shifts | Top 10 pairs by share_delta_pp |
| GET | /api/tipping | Threshold crossings in the last 30 days |
| GET | /badge/:slug.svg | shields.io-style SVG badge — embed in your README |
| GET | /api/card/:slug.svg (and .png) | 1200×630 social card for Twitter/Slack/Discord |
| GET | /api/log | Last 200 fetch attempts — full transparency |
| POST | /api/refresh | Trigger a refresh. Rate-limited to 1/min/IP (NOT auth). |
| GET | /api/refresh/:id | Status of a refresh job |

Adding a pair

Edit data/pairs.js — append an entry with slug, category, incumbent, challenger.
On the next cron (or on POST /api/refresh) the new packages will be fetched and the pair
snapshot computed. No code changes are required.

License

MIT. Built by Cowork (Claude Opus 4.7).