Zero-config code transformation with babel-plugin-macros
Babel started out as a transpiler to let you write the latest version of the ECMAScript specification but ship to environments that don't implement those features yet. But it has become much more than that. "Compilers are the New Frameworks" says Tom Dale and I could not agree more. We're seeing more and more compile-time optimizations for libraries and frameworks. I'm not talking about syntax extensions to the language, but simple code transformations that enable patterns that would be difficult to accomplish otherwise.
One of my favorite things about compiler plugins is that you can use them to optimize the user experience and developer experience at the same time. (Read more about "How writing custom Babel & ESLint plugins can increase productivity & improve user experience").
I have a few problems with Babel plugins though:
- They can lead to confusion because when looking at code in a project, you might not know that there's a plugin transforming that code.
- They have to be globally configured or configured out-of-band (in a
.babelrc
or webpack config). - They can conflict in very confusing ways due to the fact that all babel plugins run simultaneously (on a single walk of Babel's AST).
These problems could be solved if we could import Babel plugins and apply them directly to our code. This would mean the transformation is more explicit, we wouldn't need to add them to configuration, and ordering can happen in the order the plugins are imported. Wouldn't that be cool!?!?
babel-plugin-macros
🎣
Introducing Guess what! A tool like this exists! babel-plugin-macros
is a new Babel plugin that allows you to do exactly what we're talking about. It's a "new" approach to code transformation. It enables you to have zero-config, importable code transformations. The idea came from Sunil Pai and caught my attention in this create-react-app issue.
So what does it look like? Whelp! There are already a few babel-plugin-macros
packages out there you can try today!
Here's a real-world example of using preval.macro
to inline an SVG in a universal application built with Next.js:
// search.js
// this file runs in the browser
import preval from 'preval.macro'
import glamorous from 'glamorous'
const base64SearchSVG = preval.require('./search-svg')
// this will be transpiled to something like:
// const base65SearchSVG = 'PD94bWwgdmVyc2lv...etc...')
const SearchBox = glamorous.input('algolia_searchbox', props => ({
backgroundImage: `url("data:image/svg+xml;base64,${base64SearchSVG}")`,
// ...
}))
// search-svg.js
// this file runs at build-time only
// because it's required using preval.require function, which is a macro!
const fs = require('fs')
const path = require('path')
const svgPath = path.join(__dirname, 'svgs/search.svg')
const svgString = fs.readFileSync(svgPath, 'utf8')
const base64String = new Buffer(svgString).toString('base64')
module.exports = base64String
What's cool about this? Well, the alternative would look exactly like the example above except:
- It's less explicit because there would be no
import preval from 'preval.macro'
in the source code. - Have to add
babel-plugin-preval
to your babel configuration. - Need to update your ESLint config to allow for the
preval
variable as a global. - If you misconfigured
babel-plugin-preval
you'd get a cryptic runtime error like:Uncaught ReferenceError: preval is not defined
.
By using preval.macro
with babel-plugin-macros
, we don't have any of those problems because:
- The import is there and used explicitly.
babel-plugin-macros
needs to be added to your config, but only once, then you can use all the macros you'd like (even local macros!)- No need to update ESLint config because it's explicit.
- If you misconfigure
babel-plugin-macros
then you'll get a much more friendly compile time error message that indicates what the actual problem is pointing you to documentation.
So what is it really? The TL;DR is that babel-plugin-macros
is a simpler way to write and use Babel transforms.
There are already several published babel-plugin-macros
you can use, including preval.macro
, codegen.macro
, idx.macro
, emotion/macro
, tagged-translations/macro
, babel-plugin-console/scope.macro
, and glamor
🔜.
Another example
babel-plugin-macros
is a way to have no config for non-syntax babel plugins. So many existing babel plugins could be implemented as a macro. Here's another example of babel-plugin-console
which exposes a macro version of itself:
import scope from 'babel-plugin-console/scope.macro'
function add100(a) {
const oneHundred = 100
scope('Add 100 to another number')
return add(a, oneHundred)
}
function add(a, b) {
return a + b;
}
Now, when that code is run, the scope
function does some pretty nifty things:
Browser:
Node:
Cool right? And using it is just like using any other dependency, except it has all the benefits mentioned above.
Conclusion
I think we've only begun to scratch the surface of what babel-plugin-macros
can do. I'm hoping that we can land it in create-react-app so folks using create-react-app
can have even more power with zero configuration. I'm really excited to see more Babel plugins expose a macro
in addition to the existing plugin functionality they already have. I can't wait to see folks create macros that are specific to their project needs.
Creating a macros is even easier than a regular Babel plugin, but it does require a bit of knowledge around ASTs and Babel. If this is new to you, there are a, few, resources for you 😀
Good luck to you all! 👋
P.S. I should mention that language macros are not a new concept at all. Being able to teach a language new tricks has been around for a very long time. In fact, there's already such a tool for JavaScript and even one implemented as a Babel plugin already. babel-plugin-macros
takes a slightly different approach however. While macros have often been associated with defining new syntax for a language, that's not the goal of babel-plugin-macros
at all. In the case of babel-plugin-macros
it's more about code transformations.