How to write simple babel macro

stereobooster - Sep 8 '18 - - Dev Community

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");
Enter fullscreen mode Exit fullscreen mode

To be converted to

const asyncModule = import(/* webpackChunkName: MyComponent */ "./MyComponent");
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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.`,
    );
  }
Enter fullscreen mode Exit fullscreen mode

Finally AST manipulation - let's replace wcImport("./MyComponent") with import("./MyComponent");,

  referencePath.parentPath.replaceWith(
    t.callExpression(t.identifier("import"), [
      t.stringLiteral(webpackCommentImportPath),
    ]),
  );
Enter fullscreen mode Exit fullscreen mode

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];
Enter fullscreen mode Exit fullscreen mode

And put the magic component before the first argument of the import:

  referencePath.parentPath
    .get("arguments")[0]
    .addComment("leading", ` webpackChunkName: ${identifier} `);
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. inspect type of the expression with console.log(referencePath.parentPath.type) and read about it in babel.types
  2. 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.

Follow me on twitter and github.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .