All Articles

JSCodeshift and ASTs

Tree

I have been working on some open-source tools for CanJS. I am working on the migration tool which is used to help upgrade projects to the latest version. We use jscodeshift to handle these migrations and this was my first time using them and writing them.

jscodeshift allows you to create complex transformations that can read the source file and parse that into an Abstract Syntax Tree (AST). We can then manipulate it and regenerate the source code.

AST Explorer

AST Explorer has been an extremely useful tool in helping me understand AST’s and I’ve been using it to create my transforms live in the browser. It was very helpful to me to see the transformations happening as I was writing the transformation.

To enable the transforms within AST Explorer toggle the transform option within the top navigation bar. By turning this on you can select different types of transformations, as I am working with jscodeshift I use that option.

Examples

Let’s walk through a simple transformation that will transform CanJS Component into a StacheElement class.

Component.extend({
 tag: 'my-app',
 view: `<h1>Hello World!</h1>`
})

Will become

class MyApp extends StacheElement {
 static get view () {
   return `<h1>Hello World!</h1>`
 }
}
customElements.define('my-app', MyApp)

Here is a gist with the code used to do the transformation in its most basic form:

export default function transformer(file, api) {
const j = api.jscodeshift;
return j(file.source)
.find(j.CallExpression, {
callee: {
type: 'MemberExpression',
object: {
name: 'Component'
},
property: {
name: 'extend'
}
}
})
.forEach(path => {
let tagName, viewProp;
// Get the tagName
path.value.arguments[0].properties
.forEach(p => {
if (p.key.name === 'view') {
viewProp = p
}
})
// We would pascal-case the tagName
let className = 'MyApp'
// Replace the current path with a class
j(path).replaceWith(
j.classDeclaration(
j.identifier(className),
j.classBody([
j.methodDefinition(
'get',
j.identifier('view'),
j.functionExpression(
null,
[],
j.blockStatement([j.returnStatement(viewProp.value)])
),
true
)
]),
j.identifier('StacheElement')
)
)
})
.toSource();
}
view raw transform.js hosted with ❤ by GitHub

Let’s try to break this down a little, passing jscodeshift the source file will generate the AST and we then call the .find method which allows us to traverse and find something specific within the tree. It will return a Collection which you can iterate through and modify.

.find(j.CallExpression, {
 callee: {
   type: 'MemberExpression',
   object: {
     name: 'Component'
   },
   property: {
     name: 'extend'
   }
 }
})

In the above code, we are looking for a CallExpression which has a specific callee and we can specify the type and names of the callee, in this instance, we are looking for Component.extend.

Once we have the results we can iterate over them and modify to our heart’s content 😊.

We know the type of result we are going to get and the shape of it, so we can iterate over the properties of the object enabling us to add these back after we have replaced the CallExpression with a Class declaration.

We want to restore the view property and will add this to the class as a static getter property. Here we keep a reference to the view’s path.

path.value.arguments[0].properties
.forEach(p => {
   if (p.key.name === 'view') {
     viewProp = p
   }
 })

Now we can replace the CallExpression with a Class declaration:

j(path).replaceWith(
 j.classDeclaration(
   j.identifier(className),
   j.classBody([
     j.methodDefinition(
       'get',
       j.identifier('view'),
       j.functionExpression(
         null,
         [],
         j.blockStatement([j.returnStatement(viewProp.value)])
       ),
       true
     )
   ]),
   j.identifier('StacheElement')
 )
)

The first identifier is the name of the class, the second identifier is optional and it’s for adding the name of the superClass. For the classBody we will just add what was previously the value of the view property on the CallExpression.

Full working example can be seen here.

Notes

When you wish to create a Node, use the camelCase name, and when you wish to look up a Node use the PascalCase name. For example, if you wish to find a classDeclaration you would do:

j(file.source)
.find(j.ClassDeclaration)

But if you wanted to create a class you would do:

const classDeclaration = j.classDeclaration(...)

Voilà we have a simple transform!!

Thanks for following along and thanks for reading.