How to write simple babel macro
Tags: javascript, babel, macro
Published: 2018-09-08 22:32:02

---
title: How to write simple babel macro
published: true
description: A small tutorial on how to write babel  macros
tags: [javascript, babel, macro]
---

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:

```js
import wcImport from "webpack-comment-import.macro";

const asyncModule = wcImport("./MyComponent");
```

To be converted to

```js
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`](https://github.com/stereobooster/webpack-comment-import.macro/tree/boilerplate), where you can see the initial code.

```js
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](https://github.com/pveyes/raw.macro).

## Let's code

First of all get [`babel.types`](https://www.npmjs.com/package/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](https://www.npmjs.com/package/babel-types) is the most helpful reference if you want to work with babel AST.**

```js
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")`.

```js
  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"`.


```js
  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");`,


```js
  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`.


```js
  const webpackCommentImportPathParts = webpackCommentImportPath.split("/");
  const identifier =
    webpackCommentImportPathParts[webpackCommentImportPathParts.length - 1];
```

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

```js
  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:

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](https://github.com/stereobooster/webpack-comment-import.macro)

Hope it helps. Give it a try. Tell me how it goes. Or simply share ideas of you babel macros.

Follow me on [twitter](https://twitter.com/stereobooster) and [github](https://github.com/stereobooster).