Richard Tan

@richardthetan

Babel: Your first code transformations

In this tutorial, we will do some basic transforms to some source code, using Babel. Many people find the idea of transforming code scary, and unapproachable, but utilising the power of AST’s (abstract syntax trees), and a set of tools provided to us by Babel, most of the heavy lifting is done for us.

Note: The examples in the article will include code specific to react, redux, and react-redux, but familiarity with these libraries is not necessary for this tutorial.

AST Explorer

There is a website called AST explorer, that we can paste our code into and get an AST representation in many formats. This website will be useful for quickly viewing code in AST format, and will be useful when ascertaining which nodes we need to target.

Basic insertion

Below, we have a file reducers.js with a couple of imports, and a default export.

For our first transformation, lets add a new import and export to reducers.js. We’ll add mice. To do this we need to:

  1. Parse the code to AST format.
  2. Traverse the AST and find nodes adjacent to the nodes we want to add.
  3. Insert the new nodes.
  4. Generate the new code from our AST.

Here’s how we achieve this:

Let’s break this down. First of all, we call the parser on our code which transforms it from a string to an AST.

const ast = parser(file, {sourceType: 'module'});
Note, since we are using ES6 modules, we need to let the parser know with {sourceType: ‘module’}.

Next, we use traverse to find our relevant nodes. How did we know we needed ExportDefaultDeclaration and ObjectExpression? This is where AST explorer comes in handy. Below we’ve pasted our code in on the left panel, and on the right we can view the AST representation of our code.

We have 2 ImportDeclaration’s, so traverse will help us iterate over them and save the last one into a variable called lastImport. We then use insertAfter to insert the new import after the last import.

// this file is made up of snippets from transform.js
let lastImport;
traverse(ast, {
ImportDeclaration(path) {
lastImport = path;
}
const importCode = `import ${reducerName} from './${reducerName}'`;
lastImport.insertAfter(parser(importCode, {sourceType: 'module'}));

To add a property to the default exported object, we will use traverse to iterate over ObjectExpression‘s. We only expect there to be one, so we will save its properties using properties = path.parent.declaration.properties. We can then push our new mice identifier into the properties array.

// this file is made up of snippets from transform.js
traverse(ast, {
ObjectExpression(path) {
properties = path.parent.declaration.properties
}
})
const id = t.identifier(REDUCER_NAME)
properties.push(t.objectProperty(id, id, false, true))
You might be wondering what t.objectProperty(id, id, false, true) is? Good question. Since the mice alone does not have enough context, we cannot just call parser on a string of code like in the last example. Babel would parse it as an Identifier instead of a Property, leading to issues when re-generating the code. To solve this, we use the @babel/types package to help the parser understand what we have added to the AST.

Now that we have updated our AST, we can call generate on it. This will transform the code from an AST back into code in string format. We run prettier on the string, and we end up with code like below:

Wrapping a variable in a function

Next up, we will learn how to use replaceWith to wrap an identifier in a high order component.

Essentially, we want to go from:

export default Sports;

to:

const mapStateToProps = ({ volleyball, soccer }) => ({
volleyball,
soccer
});
export default connect(mapStateToProps)(Sports);

This consists of two steps.

  1. Get the name of the default export (identifier).
  2. Replace it with a wrapped version of itself, and the mapStateToProps function.

Here’s the file we will operate on:

And this is the code to transform it:

First thing to note, since we are parsing JSX this time, we need to let the parser know:

const ast = parser(file, {sourceType: 'module', plugins: ['jsx']});

This time we use traverse to iterate over the AST and find the ExportDefaultDeclaration. Once we’ve found it, we store the name of the variable being exported.

const declarationName = exportDefaultPath.node.declaration.name;

Since we know the name of the exported variable, we can now replace the entire default export with new code:

exportDefaultPath.replaceWith(
// new code...
)

Having transformed the AST, we can run generate and prettier on it, and write the file to disk. We end up with:

Summing up

Learning how to manipulate AST’s will open up many new possibilities to you. With AST’s you could write:

  • Linting plugins
  • Babel plugins
  • Codemods

I hope this tutorial will set you on your way to exploring the world of AST’s!

More by Richard Tan

Topics of interest

More Related Stories