DEV Community

Daniel Del Core
Daniel Del Core

Posted on

A new way to ship Codemods

A little under two years ago, my team was preparing for our first performance refactor of our Design System. Our plans to maximise performance gains were predicated on removing deprecated API, which had slowly accumulated over the years, now representing a substantial amount of bloat and code duplication. Worryingly, doing so would mean an unprecedented amount of breaking changes at a time when adoption and stability were a huge pain point for our consumers. Automated migration seemed like the only feasible way forward…

Like most popular libraries, e.g. React, Next.js and more, which provide codemods to help move their huge user base across versions, we needed a bespoke and fairly simple CLI wrapper of jscodeshift, that would provide a means of publishing, downloading and running codemods. So we created a custom 'codemod-cli', our first piece of internal codemod infrastructure.

We had already written a few codemods to help with some internal migrations. These were in the form of standalone transform files co-located with the packages they served but were one-offs which were written, run and forgotten about since the work was done and no longer necessary. This is not unlike what React and Next.js provide, unstructured but at times unclear as to when and why a codemod should be used. This is where we saw our first opportunity:

What if every breaking change was paired with a versioned codemod?

In this model, every breaking change would be accompanied by a codemod, the name of the codemod would point to the version which it targeted button-v2.0.0 so its intent was clear to the user. What’s more, once we accumulate codemods for multiple major versions, it would be possible for users lagging many versions behind to be slingshotted to the latest code by running all available codemods in sequence.

With this paradigm in place and the functionality implemented in the codemod-cli, we were at a point where we needed to put tools down. We needed to get back to the project we originally set out to complete, improving performance. It was now on us to start putting it to the test by writing and publishing codemods for our breaking changes. But now with the ability to actually change APIs that had been holding us back for years. To keep this short, I’ll skip over the next year or so by just saying: It worked! We wrote loads of codemods, they ran and they certainly did what we originally intended – Yay!

However, I walked away from that project with a lot of unfinished business, I felt that there were many more uncapitalised opportunities in this space. So I did what no other engineer has done before me, I started a side-project… 😅. With more and more Design Systems and convergence toward multi-package repositories, I feel like it’s the right time to share what I’ve been up to in the hope that it might also help folks in a similar situation to us.

🚚 CodeshiftCommunity

CodeshiftCommunity site

First and foremost, If the project was nothing else, it would simply be a docs site representing our collective codemod authoring knowledge. This is obviously a huge gap and barrier to entry for newcomers to jscodeshift. An onboarding experience that is quite intimidating, pieced together by various examples and blog posts.

Secondly, the strategy of codemods targeting specific versions of a package seemed strikingly similar to that of DefinitelyTyped, versioned artifacts (ts type definitions) supporting a package across its lifecycle. What if we could provide similar facilities to act as a single place where codemods could be distributed, documented and maintained in a controlled and structured way?

Thirdly, everyone using jscodeshift, including ourselves, is likely to be also writing/maintaining a bespoke CLI to solve this problem. We would need to tie everything together into a single and familiar CLI tool.

Lastly, my main objective and what I’m currently working on is to: Provide a renovate-like bot that can not only version bump, but also automatically migrate code across major versions and prompt maintainers to either merge on green CI or give them clear call-outs for their intervention in case a codemod can’t migrate them all the way.

How it works

Generally speaking, the library works by publishing every codemod to npm as its own package or piggybacking off an existing NPM package. Our CLI can then download and run them from anywhere. With the hidden benefit of being able to publish codemods with dependencies, currently, something that isn’t possible with vanilla jscodeshift CLI implementations.

Using NPM drastically lowers the adoption bar since all one would need to do is publish their existing package paired with a codemod.config.js, which is effectively a file that holds the names and locations of codemods. In existing npm packages, simply adding this file would be all that is necessary to adopt Codeshift, no dependency is required.

For example:

export.module = {
  transforms: { // Versioned transforms
    '12.0.0': require.resolve('./18.0.0/transform'),
    '13.0.0': require.resolve('./19.0.0/transform'),
  },
  presets: { // Generic utility transforms
    'format-imports': require.resolve('./format-imports/transform')
  }
};
Enter fullscreen mode Exit fullscreen mode

Running the above codemod now is a simple matter of referencing the package name and version.

Let’s for a moment say this config exists within the @chakra/button package. Assuming the config and codemods are published to NPM, one could run:

$ codeshift -p @chakra/button@12.0.0 path/to/src

Codeshift will download the latest version of @chakra/button locally, ensuring we always have the most up-to-date codemods. The CLI would read the config and pass that to jscodeshift where normal transformation would take place.

Passing the --sequence flag will trigger a run of both v12 and v13, one after the other.

The presets config, is a place for “generic” or “non-version specific” codemods, which relate to @chakra/button. This is potentially where one might want to share codemods like, format-imports which would, for example, format button imports into a certain order. Running one looks like:

$ codeshift -p @chakra/button#format-imports path/to/src

How you adopt Codeshift is up to you

You can contribute to the public registry

Augment existing packages with codemods to make them available to via the @codeshift/cli.

Or create your own private codemod registry, utilising the same docs, best practices and structured API whilst being fully compatible the community.

See the authoring guide for more info.

What next?

The overarching goal is to lower the barrier to entry so that the JS ecosystem as a whole could leverage these resources and community and in turn generate codemod coverage for libraries that we depend on, in the same way we consume type definitions from DefinitelyTyped. The idea is that developers will eventually be able to leverage codemods the community collectively provide, simplifying migration for core dependencies (react, redux, next, emotion, jest, etc). A lofty goal, but consider if existing ecosystem codemods were to integrate with the library.

The only barrier to entry for them is providing a configuration file and using the @codeshift/cli, which can be safely done alongside existing infrastructure. Once published to NPM our build tooling and consumers can run these codemods from anywhere.

Ultimately and more importantly consolidating efforts in the space into a structured library serves the broader JS ecosystem.

If you’re interested or want to learn more please feel free to browse the docs: CodeshiftCommunity.

Top comments (0)