JavaScript

Consolidating our whole JavaScript toolchain around Bun and Vite

JS

Our JavaScript toolchain had accumulated five separate tools over the years for package management, testing, bundling, dev serving, and runtime execution, each chosen independently at different points in time for reasons that made sense in isolation but added up to a genuinely fragmented developer experience. This quarter we consolidated nearly all of it around Bun and Vite, and the process was as much about paying down accumulated decision debt as it was about adopting anything genuinely new.

What the old setup actually cost us

The five-tool era wasn't the result of bad decisions, each tool had been a reasonable choice given what was available and what the project needed at the time it was adopted. But five tools chosen at five different points in time means five different configuration formats, five different sets of quirks a new engineer has to learn, and five different places a version mismatch can quietly break a build. New project setup had turned into a checklist nobody enjoyed working through, and onboarding a new engineer meant walking them through the historical reasons behind choices that no longer had a clear justification on their own merits, only inertia.

The maintenance burden was subtler but real. Keeping five tools' dependencies compatible with each other meant that upgrading any one of them carried a real chance of breaking something downstream in a way that had nothing to do with the tool being upgraded. We'd absorbed that cost as background noise for long enough that it took a genuinely fresh look at the toolchain to notice how much engineering time was quietly going toward keeping tools compatible with each other rather than toward client work.

How the consolidation actually happened

Bun now handles package installation, script running, and testing across most projects, replacing what used to be three separate tools with meaningfully different configuration formats. The installation speed improvement was immediately obvious and, honestly, the least interesting part of the change, everyone expected Bun to be fast at installing packages. What mattered more was that a single tool now handles three jobs that used to require three separate mental models, three separate config files, and three separate places version mismatches could surface.

Vite remains our dev server and bundler of choice, and its continued compatibility with Bun as the underlying runtime made the consolidation far smoother than a bigger toolchain migration usually is. We didn't have to choose between Bun and Vite, which was the biggest risk we'd flagged going in, since a forced choice between "fast runtime" and "mature bundler ecosystem" would have made this a much harder call. Vite's plugin ecosystem, which several of our projects depend on for framework-specific tooling, kept working without modification once we pointed it at Bun as the runtime underneath.

  • Bun replaced three separate tools for package installation, script running, and test execution with one consistent configuration format.
  • Vite's continued compatibility with Bun meant we didn't have to choose between runtime speed and bundler maturity.
  • Migration was incremental, project by project, rather than a single risky cutover across the whole codebase at once.
  • New project setup dropped from a multi-tool checklist to a single, consistent starting configuration.

The migration process itself

We didn't do this as a single cutover across every project at once. Migration went project by project, starting with our smallest and least business-critical internal tools to work out the rough edges before touching anything client-facing. That staged approach caught a handful of test-runner compatibility issues early, mostly around test files that relied on subtle behavior differences between our old test runner and Bun's built-in one, before those issues had a chance to show up in a project where a broken test suite would have actually blocked a client deadline.

The trickiest migrations were, unsurprisingly, our oldest projects, the ones whose original tool choices predated most of the team currently working on them. Those projects had accumulated workarounds for quirks in tools we were now removing, and untangling which workarounds were still necessary versus which were solving a problem that Bun and Vite didn't have in the first place took real archaeological effort. A few of those workarounds turned out to be compensating for bugs in tools that had since been fixed upstream years ago, meaning we'd been carrying dead weight in our config for longer than anyone realized.

What we'd tell a team considering the same move

Fewer tools means fewer configuration files to keep in sync and fewer version compatibility issues to track. The consolidation took real time upfront, more than we initially budgeted for, largely because of the archaeological work on our oldest projects, but new project setup is noticeably simpler on the other side of it, to the point where an engineer can now go from an empty directory to a running dev server with a working test suite in a few minutes rather than the better part of an afternoon.

If we were doing this again, we'd budget more time upfront for auditing exactly why each piece of existing configuration exists before assuming it can migrate cleanly. The projects where we skipped that audit and just tried to make the new tools "work" ended up needing a second pass once we discovered a workaround was hiding a real requirement rather than a historical artifact. Understanding what a piece of legacy configuration is actually protecting against, before removing it, would have saved us real time on at least two of the migrations.

Testing deserved its own section of planning that we initially underestimated. Consolidating onto Bun's built-in test runner meant retiring our previous test runner entirely on most projects, and while the two tools are broadly compatible at the level of basic test syntax, we found real differences in more advanced areas: snapshot testing behavior, certain mocking patterns, and how each runner handled test isolation between files under parallel execution. A test suite that passed reliably under the old runner occasionally behaved differently, not incorrectly, but differently enough to notice, under the new one, usually surfacing subtle test isolation bugs that had been latent all along and that the old runner's specific execution order had simply never exposed.

Rather than treating those differences as bugs in the new runner, we treated most of them as an opportunity to fix genuine, previously-hidden fragility in our own test suites. A test that only passes reliably under one specific execution order was never a fully trustworthy test to begin with, it just hadn't been caught yet. Fixing that fragility as part of the migration took longer than we budgeted, but it left every migrated project with a genuinely more robust test suite than it had going in, which is the kind of side benefit that's hard to plan for explicitly but worth acknowledging when it happens.

The consolidation also gave us a chance to simplify our continuous integration configuration, since a CI pipeline built around five different tools had accumulated a correspondingly complex set of installation, caching, and execution steps. Collapsing package installation, testing, and script execution into Bun alone let us cut our CI configuration files down substantially, and CI run times dropped as a direct consequence of Bun's faster installation and test execution, on top of whatever gains came from the tools themselves being faster in isolation. That CI simplification wasn't the primary goal of the project, but it turned out to be one of the more immediately appreciated side effects across the team, since CI configuration is exactly the kind of infrastructure everyone touches regularly and nobody enjoys debugging when it's needlessly complex.

Timing the migration around active client deadlines took more coordination than the technical work itself in a couple of cases. We deliberately avoided touching any project's toolchain during a stretch when that project had a hard client deadline within the following few weeks, even if the project would otherwise have been a good early candidate for migration, since introducing toolchain instability right before a deadline is exactly the kind of self-inflicted risk that's easy to avoid with a bit of scheduling discipline.

That meant the migration calendar ended up looking different from a purely technical prioritization would have suggested, with a couple of our more complex, higher-value projects migrating later than their technical readiness alone would have implied, simply because their delivery schedule didn't have a comfortable window until later in the quarter. We think that was the right call. A toolchain migration is valuable but rarely urgent in the way a client deadline is, and sequencing it around business priorities rather than around what would be most technically convenient for the engineering team kept the whole consolidation effort from ever becoming a source of client-facing risk.

← Back to the journal

Have a project in mind?
Let’s talk.

Tell us where you are and where you want to go. We'll map the fastest route between the two.

Currently accepting new clients