Babel is a JavaScript compiler which allows us to use the latest and greatest version of JavaScript by converting it into code that the browsers understand. It is undoubtedly one of the most powerful tools in the market to help with Frontend Development.

In this article, we will explore the basics of Babel architecture, create a basic Plugin and look into how we can test our Plugin.

Babel is different from the traditional compilers which convert the code to binary format, for instance, Babel could do:

Source transformations a.k.a Transpiling i.e. code to code transformations

Polyfill missing features

Anything else we can imagine with our source code

Babel does this through the power of Plugins and Presets. In short, Plugins are atomic components which help perform a single task. For example, there are Plugins for converting Arrow Functions to ES5 Functions, JSX to React functional calls and so on. Presets, on the other hand, are the combinations of Plugins which help simplify the workflow by avoiding installation of multiple Plugins and worrying about the invokation order. Read this article for a detailed explanation of Plugins and Presets.

Babel Architecture

Before we move on to build the sample Plugin, let us briefly take a look at the architecture of Babel which supports the use of Plugins.

There are a few main phases through which our code is passed. Some of these stages are very common to any compiler while some of these are custom to Babel.

Checkout this website which helps us visualize the next section of this article

Generating Abstract Syntax Tree (AST): The code that is passed into Babel as input is converted into a tree-like object. This stage is known as Parsing. Internally, this phase is split into two sub-phases, the first one converts the code into lexical tokens while the next phase converts these tokens to AST. Every statement (or a part of it) is converted to a token and eventually into a Node in the AST which contains information such as the type of the Node e.g. FunctionDeclaration , ReturnStatement , BooleanExpression , its start and end location within the source code and other metadata which is helpful in further understanding the Node and provides information that might be useful for processing that Node . Consuming Plugins: Once the AST is generated, the Babel compiler leverages various Plugins to transform the code from one form to another. This phase is also called Traversing. The list of Plugins/Presets to be applied to an AST are provided as Babel configuration. Plugins are applied in the order in which they are defined and Presets vice-versa. This phase essentially works on the AST and outputs another AST which consists of the changes performed by the Plugins. Generating Code: Once the AST modification is complete, the AST is converted back to code.

Although what we discussed above sounds incredibly complex. It is made relatively easy thanks to the bulk of tooling that is available for building and using Babel Plugins.

Since Plugins actually work against the AST generated from the code and not the code itself, we need to understand one last thing before moving on to the Plugin development.

To modify the AST, we need to visit the AST Nodes. Traditionally, if we have a tree-like object and we want to modify it, we would probably end up with a lodash-esque API after a few rounds of development trial and error to create/update/delete anything from the AST. But with Babel, things are slightly different with the introduction of an important topic called Visitors which are based on the Visitor Pattern. Each Visitor object consists of the methods which will be invoked when a particular Node type is encountered. If we wanted to invoke a Visitor for all Identifier and FunctionDeclaration Nodes it would look as follows:

const visitor = {

Identifier: {

enter() {

console.log("Identifier enter called");

}

exit() {

console.log("Identifier exit called");

}

},

FunctionDeclaration: {

enter() {

console.log("FunctionDeclaration enter called");

}

exit() {

console.log("FunctionDeclaration exit called");

}

}

};

Every time a Node of a certain Type is encountered, the corresponding enter method is triggered on the Visitor and then the exit method is invoked while the control exits that Node. If we do not care about what happens at exit , we can simply merge the declaration with the enter method.

const visitor = {

Identifier() {

console.log("Identifier enter called");

}

};

We know from experience that code is heavily interdependent and we simply cannot assume that there are no implicit or explicit dependencies. For this reason, Babel provides a Path alongside the AST which represents the relationship between the Nodes along with the Metadata of each of the Node. So whenever we modify the AST, the Path is updated in response to the change to the AST.

const visitor = {

Identifier: {

enter(path) {

console.log("Identifier enter called", path.node.name);

}

exit(path) {

console.log("Identifier exit called", path.node.name);

}

}

FunctionDeclaration: {

enter(path) {

console.log("FD enter called", path.node.name);

}

exit(path) {

console.log("FD exit called", path.node.name);

}

}

};

There are other advanced concepts such as scopes and bindings which we will reserve for a later article.

Sample Plugin

We are now ready to build our sample plugin. In this example, we are going to achieve a trivial goal: determine the runtime of each method within a program.

For the sake of simplicity, we are only considering a handful of examples which are rather easy (yet realistic enough) for our Plugin to process:

We have three functions which are runnings loops, evaluating If conditions, calling other functions, and contain nested functions which return references.

To be able to record the runtime of each method, we are going to use the console.time and console.timeEnd methods. The idea is simple, our Plugin will insert console.time at the top of the function and console.timeEnd right before the return statement if it exists or at the end of the function if it does not.

Project Setup

We can set up a blank NodeJS project to create a Babel Plugin. There are also Yeoman generators if you are interested in taking that route.

mkdir babel-plugin-runtime-logger cd babel-plugin-runtime-logger npm init -y

Once the project is created, create a folder src and create index.js file within. This is the file in which we will create our Plugin. The idea is straightforward:

Insert console.time(functionName) at the top of each function Insert console.timeEnd(functionName) at the end of the function before return statement (if it exists)

Creating Visitor

First, let us create a blank visitor and give it a name:

Next, we identify whenever our FunctionDeclaration is being processed. If so, we can simply insert our time statement there:

This looks a little confusing, so let us break down the enter method on our FuntionDeclaration :

W hen a function declaration is encountered (i.e. on enter) get the body of the current path (i.e. get the BlockStatement within it a.k.a the body of the function ) and before everything else (i.e. unshift into body ) insert a CallExpression (i.e. function call) which invokes the time method on the console object with the node name as a parameter

The only natural next question is: how would I know what to invoke? Which functions exist and what parameters are required? The answer is not as complex, Babel website has great documentation explaining the Types and there are helpers available to simplify any complex conditions and checks that you may need to apply. It is also strongly recommended to use the AST visualizer to help understand the more complex workflows.

Now, we can add the exit part to the FunctionDeclaration to complete the processing based on whether the last statement of the function is a ReturnStatement or not.

Based on the type of the last statement in the function, we insert the the timeEnd function call either before or after the current last statement.

With this, our Plugin development is complete. Let us now test the changes we have made so far.

Testing

To test the Plugin, we luckily don’t have to rely on real world applications. Instead, we will be using the babel-plugin-tester library along side Jest to test the Plugin.

npm i -D jest babel-plugin-tester

The Plugin Tester comes with several ways of testing the Plugin which can be seen here. But we are going to go with the option which requires minimal configuration so that we can jump in and test our Plugin. This option relies on fixtures which are nothing but pre-decided input and output combinations. As shown on the babel-plugin-tester repository:

fixtures

├── first-test # test title will be: "first test"

│ ├── code.js # required

│ └── output.js # required

└── second-test

├── .babelrc # optional

├── code.js

└── output.js

To setup our tests to run using jest , we first create a test folder alongside src and create an index.spec.js file within it:

We are invoking the pluginTester with our plugin from src and with the fixtures that we define in our fixtures folder inside test .

In this case, we are only defining one fixture called logger with the content of code.js as we discussed at the very beginning:

And now, we create the expected output.js file before running the tests and validating our assertion:

The overall directory structure of the project is quite straightforward:

project folder structure

We can now invoke these tests using jest :

./node_modules/.bin/jest test/

and the result is shown below:

All successful tests

Since all the tests run as expected, we see that the test cases pass. If there were to be any error, we would see it clearly marked thanks to the babel-plugin-tester .

Indication of unexpected results

Summary

Babel is undoubtedly a very powerful JavaScript transpiler and has a lot to offer. Refer to this link for details on the basics of the Babel compilers internal working and this link for other details on becoming a better Plugin author.

The entire codebase shown in this article is available here.