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