JavaScript

Standardizing every project, old and new, on one Vite-Bun toolchain

JS

Last year's consolidation around Vite and Bun covered our actively developed projects, the ones with regular commits and an engineer who'd notice quickly if the build broke. This quarter we finished the harder, less glamorous part: migrating the handful of legacy, lower-maintenance projects that had been left on older tooling for years because nobody wanted to spend the time, and because none of them were broken enough to force the issue.

Why We Finished The Job

A toolchain that's 90% consolidated still means a new hire needs to remember which 10% of projects behave differently, and in practice that 10% tends to be exactly the projects nobody wants to touch, which makes the knowledge gap self-reinforcing. Every project we left on old tooling was a project where onboarding took longer, where a security patch to a build dependency needed its own separate investigation, and where "just run the usual commands" quietly stopped being true. None of that was dramatic on its own. All of it added up to a tax we kept paying in small increments rather than a bill we ever consciously decided to accept.

  • Each legacy project had its own undocumented quirks in how the build was actually invoked, often via a wrapper script nobody remembered writing.
  • Security patches to old bundler dependencies required someone to relearn a project's specific setup before they could even confirm the patch applied cleanly.
  • New hires needed a mental map of which projects used which toolchain before they could safely touch anything outside their primary team.
  • Local dev environment setup instructions had silently diverged across projects, wasting time on every new laptop setup.

The Oldest Project Was The Hardest

The oldest project in our portfolio was still on a webpack configuration nobody fully understood anymore, cobbled together over several years by engineers who had since moved on, with comments referencing decisions whose original reasoning was long gone. Migrating it took real archaeology before the migration itself could even start. We spent more calendar time understanding what the existing setup actually did — which loaders were load-bearing, which plugin configurations were vestigial, which environment variables secretly changed build behavior — than we spent on the mechanical work of replacing it with a Vite equivalent.

That asymmetry, more time spent understanding the old system than replacing it, turned out to be the norm across every legacy migration we did this quarter, not the exception. It's a useful lesson for anyone tempted to estimate a toolchain migration based on the size of the new configuration file: the bulk of the effort is almost never in the destination, it's in correctly reverse-engineering everything the current setup does that isn't written down anywhere.

What The Migration Actually Involved

For each legacy project we followed roughly the same sequence: audit the existing build output to establish a known-good baseline, trace every custom loader or plugin back to the specific problem it was solving, confirm whether that problem still existed or had been solved elsewhere in the meantime, and only then write the equivalent Vite configuration. A few projects turned out to be carrying build complexity for problems that no longer applied at all — a polyfill for a browser nobody supported anymore, a workaround for a dependency that had since fixed its own bug upstream — and simply dropping that complexity was more valuable than faithfully replicating it.

  • Establish a known-good build output baseline before touching any configuration, so regressions are obvious rather than a guessing game.
  • Trace every non-obvious loader or plugin back to the original problem it solved, rather than assuming it's still necessary.
  • Drop dead complexity outright when the underlying problem no longer exists, instead of faithfully porting it forward.
  • Run the old and new build side by side for a full release cycle on lower-risk projects before fully cutting over.

What We Have Now

Every project in our portfolio now shares the same install, build, and test commands, which sounds like a small thing until you've watched a new contributor spend their first afternoon just figuring out how to run a project rather than actually contributing to it. That afternoon is gone now, across every project, not just the newer ones. Spinning up a contributor on any project, old or new, is meaningfully faster than it was a year ago, and the support burden on the few engineers who used to be the informal keepers of legacy build knowledge has dropped correspondingly.

A single, consistent toolchain across the entire portfolio was worth the multi-quarter effort, even though no individual week of that effort felt urgent while we were doing it. The value was never really about Vite or Bun as specific tools, either of which we'd happily swap for a comparably solid alternative if one emerged. The value is in never again having to remember which project uses which tool, which build command works where, or which quirks apply only to the project nobody's touched since 2023. That's a boring kind of win, and boring is exactly what we were going for.

Prioritizing Which Legacy Project To Migrate First

We didn't tackle the remaining legacy projects in a random order, and getting the sequencing right mattered more than we expected. Our first instinct was to start with the smallest, simplest project on the list, on the theory that an easy win would build momentum. That instinct was wrong. The smallest project turned out to be low-value to migrate precisely because it was small and rarely touched, and finishing it first taught us almost nothing transferable to the harder projects still waiting.

We switched to prioritizing by a combination of how often a project was actually touched and how much undocumented complexity it carried, migrating the projects that were both frequently modified and structurally messy first, since those were exactly the projects where the old tooling was actively costing us time every week rather than sitting quietly unused. That reordering meant our second migration was harder than our first would have been under the original plan, but every lesson learned on it applied directly to the genuinely difficult, oldest project we tackled last, by which point we'd built real fluency in reading unfamiliar legacy build configurations quickly.

What Surprised Us About Bun Specifically

Bun's package installation speed was the headline reason we adopted it in the first place, and it delivered exactly as advertised across every migration. What surprised us more, revisiting these older projects, was how many of them had accumulated dependency resolution workarounds, pinned sub-dependency versions, manual patches applied via postinstall scripts, that existed only to work around bugs in an older package manager's resolution algorithm. Once we swapped in Bun's resolver, several of those workarounds turned out to be entirely unnecessary, and removing them alongside the toolchain migration meant each project came out the other side not just faster to build but genuinely simpler than before.

  • Prioritize migrations by combining modification frequency with structural complexity, not by apparent ease, since the easy projects rarely teach you anything useful for the hard ones.
  • Expect to find dependency workarounds that were only ever needed because of quirks in the old tooling, and remove them rather than faithfully porting them forward.
  • Budget real calendar time for understanding an old build before touching it, since that understanding phase consistently took longer than the actual migration work itself.
  • Run old and new builds side by side on lower-risk projects before fully committing, since a silent output difference is far easier to catch that way than after the old tooling is already gone.

Why We Didn't Just Leave The Old Projects Alone

The obvious counter-argument, one we heard internally more than once during planning, was that a stable legacy project generating no complaints is a project that doesn't need engineering attention diverted to it, regardless of how mismatched its tooling is with everything else. We took that argument seriously rather than dismissing it, and for a couple of the smallest, most dormant projects we ultimately agreed with it and left them alone, at least for now.

Where the argument broke down was for projects that were dormant in terms of feature work but not dormant in terms of dependency risk. A project nobody actively develops still accumulates security advisories against its dependencies over time, and a project on old, poorly understood tooling makes patching those advisories slower and riskier than it needs to be, precisely because nobody currently on the team has fresh context on how a patch might interact with an unusual build configuration. That risk compounds quietly in the background of a project that otherwise looks perfectly stable, right up until a patch is genuinely urgent and the unfamiliar tooling turns a same-day fix into a multi-day investigation.

That reasoning is what ultimately tipped the decision toward migrating even the projects with no active feature development, provided they still received any dependency patching at all. A project genuinely frozen enough to need zero future patches is rare in practice, and for everything else, standardized tooling turned what used to be an occasional multi-day archaeology exercise into the same routine patch process we already run everywhere else.

← 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