Macro is a small program which you can write to manipulate the source code of your application at transpilation (compilation) time. Think of it as a way to tweak how your compiler behaves.
babel-plugin-macros
is a plugin for babel, to write macros for JavaScript (or Flow). The juicy part here is that as soon as babel-plugin-macros
included you don't need to touch babel config to use your macros (contrary to other babel plugins). This is super useful in locked setups, like Creat React App. Also, I like that it is explicit - you clearly see where the macro is used.
Task
I picked up toy size problem which is easy to solve with macro.
When you use dynamic import
in Webpack it will generate hard readable names for chunks (at least this is what it does in CRA), like 1.chunk.js
, 2.chunk.js
. To fix this you can use the magic comment /* webpackChunkName: MyComponent */
, so you will get MyComponent.chunk.js
, but this annoying to put this comment by hand every time. Let's write babel macro exactly to fix this.
We want code like this:
import wcImport from "webpack-comment-import.macro";
const asyncModule = wcImport("./MyComponent");
To be converted to
const asyncModule = import(/* webpackChunkName: MyComponent */ "./MyComponent");
Boilerplate
So I want to jump directly to coding, so I won't spend time on boilerplate. There is a GitHub repo with the tag boilerplate
, where you can see the initial code.
export default createMacro(webpackCommentImportMacros);
function webpackCommentImportMacros({ references, state, babel }) {
// lets walk through all calls of the macro
references.default.map(referencePath => {
// check if it is call expression e.g. someFunction("blah-blah")
if (referencePath.parentPath.type === "CallExpression") {
// call our macro
requireWebpackCommentImport({ referencePath, state, babel });
} else {
// fail otherwise
throw new Error(
`This is not supported: \`${referencePath
.findParent(babel.types.isExpression)
.getSource()}\`. Please see the webpack-comment-import.macro documentation`,
);
}
});
}
function requireWebpackCommentImport({ referencePath, state, babel }) {
// Our macro which we need to implement
}
There are also tests and build script configured. I didn't write it from scratch. I copied it from raw.macro.
Let's code
First of all get babel.types
. Here is the deal: when you working with macros, mainly what you do is manipulating AST (representation of source code), and babel.types
contains a reference to all possible types of expressions used in babel AST. babel.types
readme is the most helpful reference if you want to work with babel AST.
function requireWebpackCommentImport({ referencePath, state, babel }) {
const t = babel.types;
referencePath
is wcImport
from const asyncModule = wcImport("./MyComponent");
, so we need to get level higher, to actual call of function e.g. wcImport("./MyComponent")
.
const callExpressionPath = referencePath.parentPath;
let webpackCommentImportPath;
Now we can get arguments with which our function was called, to make sure there is no funny business happening let's use try/catch
. First argument of function call supposes to be a path of the import e.g. "./MyComponent"
.
try {
webpackCommentImportPath = callExpressionPath.get("arguments")[0].evaluate()
.value;
} catch (err) {
// swallow error, print better error below
}
if (webpackCommentImportPath === undefined) {
throw new Error(
`There was a problem evaluating the value of the argument for the code: ${callExpressionPath.getSource()}. ` +
`If the value is dynamic, please make sure that its value is statically deterministic.`,
);
}
Finally AST manipulation - let's replace wcImport("./MyComponent")
with import("./MyComponent");
,
referencePath.parentPath.replaceWith(
t.callExpression(t.identifier("import"), [
t.stringLiteral(webpackCommentImportPath),
]),
);
Let's get the last part of the path e.g. transform a/b/c
to c
.
const webpackCommentImportPathParts = webpackCommentImportPath.split("/");
const identifier =
webpackCommentImportPathParts[webpackCommentImportPathParts.length - 1];
And put the magic component before the first argument of the import:
referencePath.parentPath
.get("arguments")[0]
.addComment("leading", ` webpackChunkName: ${identifier} `);
}
And this is it. I tried to keep it short. I didn't jump into many details, ask questions.
PS
Babel documentation is a bit hard, the easiest way for me were:
- inspect type of the expression with
console.log(referencePath.parentPath.type)
and read about it inbabel.types
- read the source code of other babel-plugin which doing a similar thing
The full source code is here
Hope it helps. Give it a try. Tell me how it goes. Or simply share ideas of you babel macros.