From manual fixes to automatic upgrades — building the Codemod Platform at Lyft

With the right library, you can automate almost any change, but handling all edge cases takes time and patience.

Our Goals

To tackle this challenge, we started with several clear goals:

  • Automate dependency upgrades: minor version updates often introduce new features, while major versions come with breaking changes that require fixes. Instead of developers manually reading docs, testing changes, and updating code, we would love codemods to handle everything automatically — seamlessly upgrading APIs and applying necessary fixes without human intervention.
  • Make codemods easier to write: reduce the learning curve by providing helper functions and clear documentation, making it easier for developers to create their own transforms.
  • Make it accessible to all developers: to ensure codemods could run anywhere with Node.js access, we provide our own CLI tool. By executing it with npx, developers can run codemods without needing a global installation or adding them to frontend (FE) repositories.
  • Standardize codemods across Lyft: last but not least, we aimed to unify the different codemod implementations across Lyft.

Requirements

Choosing the right library was key. There aren’t many options for transforming code in frontend, and jscodeshift was the best fit — it provides parsing, transformation, and writing in one place. Since we needed to handle TS, TSX, JS, and JSX files, jscodeshift worked out of the box. However, it also had some limitations we wanted to address:

  • Reusing code: out of the box, jscodeshift provides a CLI to run a single transform module. However, we wanted more flexibility — allowing one transform to execute another or run multiple transformations together.

  • Detecting eligible services: for dependency upgrades, we needed to skip services that didn’t use the dependency to avoid wasting resources. This was especially important when running codemods across hundreds of microservices in CI.

  • Evergreen codemods: some transforms need extra setup before running. For example, upgrading @lyft/service (core library for Lyft FE services, learn more) to v2 requires installing sass to avoid breaking changes. Since every @lyft/service version from v2 onward has this requirement, we needed a way to ensure it was always handled automatically. We introduced evergreen codemods — pre-checks that set up things before running the main transform. We later expanded this to include post-transform checks as well.

  • Non-JS transforms: In addition to TS and JS files, we wanted to support transformations for YAML, JSON, and .env files. This allowed us to extend codemods beyond code changes, handling configuration updates, environment variable adjustments, and other use cases, making the platform even more powerful.

  • Naming convention: A clear and consistent naming convention makes the CLI easier to use and understand. To keep codemods predictable and maintainable, we established a set of naming rules:– Clear and descriptive — the name should clearly state what the transformation does, use a predictable format like verb-noun or verb-noun-adjective to make names easy to interpret.

    – Versioning — if a codemod applies to a specific library update, include the version number (e.g., react-18.3.1, next-15.1) to a transform name.

    – Allowed characters — use only lowercase letters, numbers, “.” and “-” (e.g., react-18.3.1 instead of react_1831). This prevents issues with version mix-ups, such as distinguishing 18.3.1 from 1.83.1.

  • Run 3rd-party codemods: many Frontend open-source libraries provide their own codemods to simplify migrations. For example, Next.js has the @next/codemod CLI; React and Storybook offer similar tools. We wanted our Codemod Platform to support running these existing transforms, allowing us to reuse them instead of reinventing the wheel.

  • Helper functions: last but not least, we aimed to minimize boilerplate. Whether adding a new import or removing a JSX prop from a React component, we wanted reusable functions to handle these common tasks automatically.

Design

To address these challenges, we built a solution that covers all these gaps. Here’s how we did it:

@lyft/codemod CLI

A typical codemod execution from the terminal would look like this:

For the CLI to find and execute the correct transform, we created a helper function called executeUpgrade:

As you might have noticed, we execute the UpgradeClass, which sits at the core of our platform. This class was designed to handle all the requirements we outlined, ensuring every transformation runs smoothly and consistently. Let’s take a closer look at its implementation:

To see how everything fits together, let’s walk through a real example.

Suppose we have a component library called core-ui, and in version 2, we removed the compact prop from the Button component. Our goal is to create a @lyft/codemod transform to automatically fix this breaking change.

To start, we create a new folder in our transforms directory:

The index.ts file will export a class extending UpgradeBase with the following implementation:

The transform-buttons.ts file follows the same structure as a standard jscodeshift transform module. This made migrating existing jscodeshift transforms from other projects as simple as copying and pasting them into the new platform.

Or, even better if using helper functions to reduce boilerplate:

Execution flow

  1. @lyft/codemod -t core-ui-2 is being executed
  2. executeUpgrade searches for a folder named core-ui-2
  3. core-ui-2 must contain an index.ts file exporting UpgradeBase as the default
  4. UpgradeBase.execute is called to run the codemod
  5. Execute checks if the service is eligible
  6. If eligible, runUpgrade executes the transform-buttons transformation
  7. The compact prop has been removed from the Button component across all files under the pathname
  8. 🎉 Profit

This example covers a single use case, but multiple transformations can be combined within one folder and executed sequentially. Now, imagine applying this across hundreds of microservices!

More complex use cases

Some codemods could be a lot more complex. For example, it could be running one codemod, and then executing another transform file, and then running 3rd-party codemod. All these can be easily handled by the platform. Here’s the example of the index file of the transform as well as how the whole file structure would look like:

It could be much more than that, but hopefully, this example gives you a sense of what codemods can achieve.

Releasing

The Codemod Platform is essentially a library with its own CLI. To manage different versions, we package it as an internal npm package called @lyft/codemod, which any Lyft developer can execute using:

Versioning follows semver to maintain consistency. Locally, developers can use any available version by specifying it explicitly:

In CI, we run:

This ensures that CI always uses the latest version of codemods, instantly applying all new transforms and bug fixes.

Keeping the @lyft/codemod package separate from our internal libraries has also been key. It allowed us to develop and iterate quickly, without being blocked by or introducing changes to library code.

Testing

To test our changes before it goes out we use defineTest from jscodeshift because it makes testing codemods simple and readable. It compares two fixture files — one before and one after the transform — to verify that the changes work as expected.

For example, transform-buttons fixtures structure and content would look like this

Making it very easy to maintain and read by developers!

Another useful tool for testing and writing codemods is AST Explorer, which we use frequently. Since jscodeshift transforms code into an AST before modifying it, using the right API and node types is crucial.

AST Explorer makes this easy by providing a visual representation of the AST and a real-time editor to experiment with transformations. For example, here’s how console.log(‘Hello, World’) looks in AST world:

Even though the AST might look complicated at first, it is a powerful tool that can help you analyze and transform the source code in a structured way. As you can see, each node has a type, and you can traverse the tree to find the nodes and methods you are interested in.

Outcome

So, what did we actually get out of this? Was all the effort really worth it? Let’s break it down!

Converted multiple Web Platform releases from major to minor

By fully automating breaking changes, we turned what would have been major releases into minor ones. Even better, for minor updates, we made Refactorator pull requests (PRs) fully auto-mergeable, meaning engineers no longer had to review PRs, read docs, or manually test changes.

At Lyft, Refactorator is our internal tool that automates large-scale code changes by generating and managing PRs across all projects.

With 100+ FE microservices and multiple releases per year, this cut down on tedious manual work, saved thousands of developer hours, and made upgrades a lot smoother.

Integrated codemods into automatic dependencies upgrade

At Lyft, we already had a system for automating npm dependency upgrades, but version bumps alone weren’t enough — breaking changes still needed manual fixes. By integrating the Codemods CLI into the process, upgrade PRs now do more than just update dependencies. They fix breaking changes and adopt new features automatically, all in a single PR.

For example, automating 80% of breaking changes in our components library significantly boosted adoption, leading to ~30% of microservices migrating within 2 weeks — something that would have previously taken months. This not only made upgrades smoother but also gave developers a way to write their own codemods, making it easier to contribute meaningful changes and grow their impact.

Codemods have now been executed in thousands of dependency upgrades to fix breaking changes and support new feature adoption. This automated process has helped reduce the total number of outdated dependencies across microservices by over 1,000. While this number is dynamic — since new library versions are released daily — the combination of automated package upgrades and codemods has had a lasting impact on keeping our ecosystem up to date.

Future plans

To make an even bigger impact, we’ve built a set of cleanup codemods that run on a schedule across FE services. These handle tasks like removing duplicate TypeScript compiler options, cleaning up redundant ESLint rules already covered by our base config, or eliminating unexpected duplicate dependencies in FE services.

We’re expanding codemods into this space as well. So far, we’ve created 40+ transforms in just a year, and we’re just getting started.

Looking ahead, we plan to make codemods even more accessible by integrating them into local development workflows and CI pipelines, giving engineers early feedback. We also plan to explore AI-assisted codemods that can suggest or even generate code transformations based on diff patterns, upgrade guides, or documentation. This could further reduce engineering effort and unlock new levels of automation in how we maintain and modernize our codebase.

Главная - Вики-сайт
Copyright © 2011-2025 iteam. Current version is 2.143.0. UTC+08:00, 2025-05-02 20:21
浙ICP备14020137号-1 $Гость$