Writing a Babel Plugin with Grandma How to Write a Custom Babel Plugin šµš
Posted on by Stephen CookI wrote a custom Babel plugin that lets you write React with emoji. It was not a productive use of time.
How To Write a Babel Plugin
In this post Iām going to walk you through how to make a Babel plugin, so you can make your own custom one, like grandmas-own-jsx.
I suppose you could also make a plugin thatās actually useful. I meanāāāI canāt physically stop you.
What is Babel?
Before we add anything to Babel, we first need to understand it a bit. I assume youāre roughly familiar with Babel (if not, give this a read first )āāābut letās break it down a bit.
At its heart, Babel does these 3 things: parse, transform, and generate.

In other words, it parses our input code into an Abstract Syntax Tree (AST), transforms that AST into a different shape, and then generates code from the transformed AST.
āASTā sounds confusing, but itās just a fancy way of saying ācode, but represented as a treeā.
What is a Babel Plugin
A Babel plugin is code that we can add into the transform step. Itās important to note that this is the only step that we can influence. We canāt do anything to the parse step, which means we canāt add custom syntax.
In other words, weāre limited to transforming from valid JavaScript, to valid JavaScript. If we want to transform something that isnāt valid JavaScript, we would need to modify Babel itself.
How Do We Transform Code
To transform the code, we need to traverse the AST we get given from the parse step. We can traverse this using a visitor. Letās look at this simple Babel plugin:
module.exports = function () {
const SimpleVisitor = {
StringLiteral(path, state) {
if (path.node.value === "We'll never survive!") {
path.node.value = "Nonsense. You're only saying that because no one ever has.";
}
},
};
return { visitor: SimpleVisitor };
};
Now, we could write code like this:
function fireSwamp () {
return "We'll never survive!";
}
And it would run through Babel with our plugin, and compile to this:
function fireSwamp () {
return "Nonsense. You're only saying that because no one ever has.";
}
Whatās happening here? The visitor is an object mapping from the name of an AST node, to a function describing what to do with that node. So when Babel sees the matching StringLiteral
, our plugin kicks in and transforms the stringās value.
How Do We Transform More Code?
So weāve looked at a really simple Babel plugināāāletās make something a bit more complicated. Weāll break down the code in the grandmas-own-jsx plugin , step by step.
module.exports = function ({ types: t }) {
const GrandmaVisitorInitiator = {
Program(path) {
const commentLineTokens = path.parent.comments.filter(
(token) => token.type === "CommentLine"
);
const commentBlockTokens = path.parent.comments.filter(
(token) => token.type === "CommentBlock"
);
if (!commentLineTokens.length || !commentBlockTokens.length) return;
const grandmasReference = buildGrandmasReference(commentLineTokens);
const grandmasRecipes = buildGrandmasRecipe(commentBlockTokens);
path.traverse(GrandmaVisitor, {
grandmasReference: grandmasReference,
grandmasRecipes: grandmasRecipes,
});
},
};
return { visitor: GrandmaVisitorInitiator };
};
Since grandmas-own-jsx
leverages comments, the first thing we do is look through the CommentLine
and CommentBlock
elements in the Program
node. We use these comments to get the information we need to build our React elements later on.
Next, we set up a new visitor to run on the rest of the tree. This lets us pass down state as we traverse the tree, without depending on global state.
path.traverse( // traverse the tree down from the current node
GrandmaVisitor, // the new visitor
{ ... } // the state our new visitor should get
);
So letās take a look at the new visitor:
module.exports = function ({ types: t }) {
const GrandmaVisitor = {
StringLiteral(path, state) {
if (path.node.value === "šµ") {
const recipeRef = state.grandmasRecipes[path.node.loc.start.line];
const recipeMatches = recipeRef && recipeRef.start > path.node.start;
if (recipeMatches) {
const recipe = recipeRef.value;
const domStruc = cookRecipe(recipe, state.grandmasReference);
const typeExpression = genTypeExpression(domStruc);
path.replaceWith(typeExpression);
}
}
},
};
const GrandmaVisitorInitiator = { /* ... */ };
return { visitor: GrandmaVisitorInitiator };
};
Here, we first look for any string that is just "šµ"
in our program. From the state that we were passed from the GrandmaVisitorInitiator
, we grab the reference of what React elements are meant to be there.
Then, we generate a new sub-tree with genTypeExpression
(weāll look at that in a moment).
Finally, we replace the current node (the "šµ"
string) with the new sub-tree using replaceWith
. In other words, weāre performing a transform like this:

Generating Sub-Trees (Type Expressions)
In the previous step we created a whole new sub-tree of our AST using genTypeExpression
. Babel lets us generate these AST sub-trees using type expressionsāāāletās dig into how create these using the types
builders that Babel provides:
module.exports = function ({ types: t }) {
const genTypeExpression = (node) => {
return t.callExpression(
t.memberExpression(t.identifier("React"), t.identifier("createElement")),
[
/^[A-Z]/.test(node.type[0])
? t.identifier(node.type)
: t.stringLiteral(node.type),
t.objectExpression(node.args),
...node.children.map(genTypeExpression),
]
);
};
const GrandmaVisitor = { /* ... */ };
const GrandmaVisitorInitiator = { /* ... */ };
return { visitor: GrandmaVisitorInitiator };
};
By calling e.g. t.callExpression
we create a CallExpression
node. Similarly, to create a StringLiteral
we would call t.stringLiteral
.
To build a particularly complex tree, I would strongly recommend first using something like AST explorer to get the AST node names, and then find the corresponding builder in the babel-types documentation, so you know which arguments are needed.
Summary
You should now know:
- What Babel does (parse, transform, generate)
- To use AST explorer to view codeās corresponding AST
- How to create a visitor to traverse and modify an AST
And thatās it! Go forth and create your own Babel plugins! You could turn CSS comments into real CSS, transpile import statements to deferred require statements, or maybe⦠just maybe⦠make a stupid alternative to JSX that depends heavily on the šµ emoji.
I made my own babel plugin so I can stop writing JSX and just write emoji instead.
ā Stephen Cook (@StephenCookDev) April 5, 2020
I'm not doing so well with the isolation. https://t.co/EzbqLDMiJC

Iām Stephen. I gave up a promising Mario Kart career in 2014, to instead focus on software engineering full-time.
Why not follow me on Twitter?