Programmatically convert ES5 to ES6
Working in startup means a lot of multitasking on a daily basis, and is especially true in small teams. Banging out features, fixing production bugs, answering engineering questions or (once in a while) troubleshooting deployment problems, one shuffles through these tasks sometimes multiple times a day. At work, we use primarily Javascript. The community is super vibrant. Companies are pushing the boundaries on JS engines. New frameworks and libraries are being pushed out almost on weekly (daily?) basis. The pace of change is exciting(!) but also overwhelming at the same time. The latest one being ES6.
It’s easy to write ES6 for new codebase (thanks to library like https://babeljs.io/), but what about legacy codebase that’s been maintained in ES5(or lower)? Arbitrary codebase could be huge, so manual rewrites could take days or weeks (including learning curve). Another problem is helping everyone on the team adopt ES6. Every person has different learnings styles. The straightforward way is to read the documentation and remember to apply next time. It works but not very effective, just like how rote memorisation works for college students. Now, how do you ship features, fix bugs and actively catch up with ES6 at the same time?
The answer is probably not surprising—Automation.
Recast is an amazing tool by Ben Newman for parsing and modifying Javascript code. What’s great about is that it provides two simple APIs to easily get started with: require(“recast”).parse and require(“recast”).print. The parse() call converts code string to an AST(Abstract Syntax Tree) object and from that point on, you can do whatever you want with it by traversal. Once you’re done, simply call print() to see the modified code or write to file.
Here’s a simple example:
var Foo = require('foo') => import Foo from 'foo'
AST for the former is:
VariableDeclaration {
declarations: [
<VariableDeclarator> {
id<Identifier> {
name: "Foo"
}
init<CallExpression> {
callee: {
name: "require"
}
arguments: [
<Literal> {
value: "foo"
}
]
}
}
]
}
And the latter is:
ImportDeclaration {
specifiers: [
<ImportDefaultSpecifier> {
id<Identifier> {
name: "Foo"
}
}
]
source<ModuleSpecifier>: {
value: "foo"
}
}
Above two illustrations are simplified representations of the actual AST objects provided by recast. As you can see, it’s easy to identify the key nodes in the AST for conversion: identifier Foo and literal “foo”. Once these are identified, simply use the correct builder objects to construct the desired syntax. In the latter AST above, the builder objects used are ImportDeclaration and ImportDefaultSpecifier. Calling print() on that modified structure will yield “import Foo from “foo”. Very straightforward. Equipped with these concepts, one can mold/change/recast Javascript codebase to desired syntax.
This automation process obviously solves the problems mentioned above. With carefully constructed script, it only takes one command to convert codebase of any size to ES6. On top of that, everyone on the same team can learn ES6 faster by working in the ES6-converted codebase. What better way to learn something than by imitation? It’s a #win for everyone.
However, there are limitations. It’s important to modify the AST in a way such that it doesn’t change the semantics. For the purpose of showcase, I’ve packaged up the conversion scripts I’ve used on our codebase into a tool called 5to6. Of course, it only converts a subset of code to ES6, at least what we needed only anyways. It’s also initially done as a proof-of-concept experiment, so there’s definitely room for improvements.
When working in a team with limited resources and time, it’s important to focus on work that has the highest leverage. Building automation tool is one of them. It won’t make you a 10x engineer, but definitely is one of what it takes to become one.