Sarup's blog!

about ideas

Writing a Babel codemod plugin

23 Jun 2017

If you haven’t already, you first want to check out the Babel Handbook, a fantastic document that walks you through the way Babel is designed. There is even a section on Writing a Babel plugin, however for me a newbie, the foo === bar example was a bit too simple.

To do a quick recap, Babel goes through 3 primary stages - parse, transform and generate.

  1. Parse - Take in code to transform, identify familiar tokens, and generate an Abstract Syntax Tree
  2. Transform - Traverse the AST from the previous step, and modify it into the AST representation for the transformed code
  3. Generate - Take in the final AST and convert it into transformed code

A Babel plugin comes in as part of the transform stage. Here’s what a plugin export file looks like on the inside, just for illustration:

Every plugin has a visitor property that describes traversal order and modifications to the original AST. The visitor description leverages babel-types, a library that helps a plugin author identify what kind of Node one is dealing with, its properties etc, and information for Babel to process, validate, and transform it.

You can read in length about the context of the plugin we’re going to write today, but in a nutshell, we want to be able to transform the core plugins within Babel to contain a name. e.g for the plugin export file described above, we want to modify it into:

So in the two examples above, we want to get into the default export function, find the object being returned, and inject the name as the first property.

At least that’s what I concluded and coded out the first version. Soon enough, Henry pointed out some examples of plugin export files that don’t follow that structure.

On first glance, it appears like a crazy feat, those files look pretty different. As he hints however, we can indeed just look for the visitor property, and plug name as another property to the parent object.

Not so soon 😉 Here’s an example where this logic won’t apply:

Well it does look like we can apply our original idea for files that follow the structure above. Let’s work with both approaches, and hopefully they cover most file structures. Skimming through the plugin handbook quickly, let’s start with this template:

To make a better mental model for approaching the traversal, we want to visualise first the AST we’re dealing with. To do this, we can plug our initial source code into an AST explorer.

Some tinkering around the interface reveals AST Explorer highlights the Nodes for us.

Screenshot of an AST Explorer highlight

On the AST, click through the part that says ExportDefaultDeclaration, expanding your way until you arrive at the ObjectExpression we’re targeting. That’s exactly how I made a mental model for approaching this plugin. Next you’ll need to spend some time skimming through the Babel Types README. This will help you familiarise with the various types and properties available. Here on, it’s pretty simple.

We want to traverse the AST using Babel’s APIs, such that we arrive at the ObjectExpression we found earlier. By repeatedly plugging in some console.log() statements, you can arrive at the following:

We’ll make those chained .get() statements compact, and finally reading through the Transformation Operations section in the handbook, we realise that inserting a sibling node is exactly what we want to do.

We’re therefore done with our first iteration!

That solved for one file structure case, and now we want to account for the (hopefully easier) case Henry pointed out, the one where we want to plugin the name right before the visitor.

On the outset, that should be much easier to visualise, and it’s easy to come up with a good first draft for the visitor, such as this:

The observant will note that we want to prevent inserting name twice, in the event that a file structure contains both types of nodes. Therefore, we’ll do a little name check. With some standardisation of variable names, and sharing the name check, our final plugin looks as follows:

That is it! I’ve published the same code as an npm package if you’re keen to take a look. If you clone it and follow the instructions on the README, you should be able to see it in action!

The best part for me throughout this learning exercise was that I realised at the end I forgot to add a name property to this plugin 😄 Guess what I’m gonna run 😈

$ codemod --require babel-register -o index='{"pluginName": "babel-plugin-add-name-to-plugin"}' --plugin src/index.js src/index.js

Always be eating your own dog food!