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.
Top comments (9)
Hi, how do you unit test this Babel macro?
With AVA and snapshots. See here
When I try to run the tests in IDEA, using Jest, I get "Empty tests suite", and it says that the tests passed: 0 of 2 tests, and it's also not producing any snapshots. Is there any other way to run them?
Oh wait a second you right it is Jest. Why I thought it was AVA? Anyway, you should be able to run
npm test
in console.Do you know maybe how can I import several custom babel-macros from one place in my project (For example index.js)?
As far as I know this is not possible, this kind of contradicts the idea of macro. You need explicitly declare in each file which macro you want to use. If you want something to be applied everywhere you need to use babel-plugin for this.
I can be wrong here. You can open issue in babel-macro repository with this question.
Thank you for your answers!
Is that possible to at least to import the macro from an absolute path instead of the relative one?
Yes, should be possible. I don't see why not
define for JS. Love it!