Kent C. Dodds News

Subscribe
Archives
May 21, 2018

Write your own code transform for fun and profit

> With babel-plugin-macros

🚨 DON’T MISS THE OPPORTUNITY TO WATCH 2 OF MY FRONTEND MASTERS COURSES ABSOLUTELY FREE FOR THE NEXT 3 DAYS 🚨 Learn more.

✨ Also, I’m starting something new! DevTips with Kent: Every weekday I’ll livestream a ~3 minute video giving development tips and things that I’m learning. Subscribe and hit the notification bell to be notified when I’m streaming!

If you haven’t heard, babel-plugin-macros “enables zero-config, importable babel plugins.” A few months ago, I published a blog post about it on the official babel blog: “Zero-config code transformation with babel-plugin-macros”.

Since then, there have been a few exciting developments:

  1. You can use it with a create-react-app application (v2 beta) because it’s now included by default in the beta version of babel-preset-react-app (which is what create-react-app v2 beta is using!)
  2. It was added as an optional transform to astexplorer.net by @FWeinb

Up until now, only early adopters have tried to write a macro, though there are a fair amount of people using the growing list of existing macros. There are tons of awesome things you can do with babel-plugin-macros, and I want to dedicate this newsletter to showing you how to get started playing around with writing your own.

Let’s start off with a contrived macro that can split a string of text and replace every space with 🐶. We’ll call it gemmafy because my dog’s name is “Gemma.” Woof!

  1. Go to astexplorer.net
  2. Make sure the language is set to JavaScript
  3. Make sure the parser is set to babylon7
  4. Enable the transform and set it to babel-macros (or babel-plugin-macros as soon as this is merged)

Then copy/paste this in the source (top left) code panel:

import gemmafy from 'gemmafy.macro'

console.log(gemmafy('hello world'))

And copy/paste this in the transform (bottom left) code panel:

module.exports = createMacro(gemmafyMacro)

function gemmafyMacro({ references, state, babel }) {
  references.default.forEach(referencePath => {
    const [firstArgumentPath] = referencePath.parentPath.get('arguments')
    const stringValue = firstArgumentPath.node.value
    const gemmafied = stringValue.split(' ').join(' 🐶 ')
    const gemmafyFunctionCallPath = firstArgumentPath.parentPath
    const gemmafiedStringLiteralNode = babel.types.stringLiteral(gemmafied)
    gemmafyFunctionCallPath.replaceWith(gemmafiedStringLiteralNode)
  })
}

> Alternatively, you can open this

TADA šŸŽ‰! You’ve written your (probably) very first babel plugin via a macro!

Here’s the output that you should be seeing (in the bottom right panel):

console.log("hello 🐶 world")

You’ll notice that babel-plugin-macros will take care of removing the import at the top of the file for you, and our macro replaced the gemmafy call with the string.

So here’s your challenge. Try to add this:

console.log(gemmafy('hello world', 'world goodbye'))

Right now that’ll transpile to:

console.log("hello 🐶 world")

Your job is to make it do this instead:

console.log("hello 🐶 world", "goodbye 🐶 world")

From there, you can play around with it and do a lot of fun things!


If you want to see more of the capabilities, then copy this in the source (top left):

import myMacro, { JSXMacro } from 'AnyNameThatEndsIn.macro';
// (note: in reality, the AnyNameThatEndsIn.macro should be the name of your package
// for example: `codegen.macro`)
const functionCall = myMacro('Awesome');
const jsx = <jsxmacro cool="right!?">Hi!</jsxmacro>;
const templateLiteral = myMacro`hi ${'there'}`;
literallyAnythingWorks(myMacro);

And copy/paste this in the transform (bottom left) code panel:

module.exports = createMacro(myMacro);

function myMacro({ references, state, babel }) {
  // `state` is the second argument you're passed to a visitor in a
  // normal babel plugin. `babel` is the `@babel/core` module.
  // do whatever you like to the AST paths you find in `references`.
  // open up the console to see what's logged and start playing around!

  // references.default refers to the default import (`myMacro` above)
  // references.JSXMacro refers to the named import of `JSXMacro`
  const { JSXMacro = [], default: defaultImport = [] } = references;

  defaultImport.forEach(referencePath =&gt; {
    if (referencePath.parentPath.type === "TaggedTemplateExpression") {
      console.log("template literal contents", referencePath.parentPath.get("quasi"));
    } else if (referencePath.parentPath.type === "CallExpression") {
      if (referencePath === referencePath.parentPath.get("callee")) {
        console.log(
          "function call arguments (as callee)",
          referencePath.parentPath.get("arguments")
        );
      } else if (referencePath.parentPath.get("arguments").includes(referencePath)) {
        console.log(
          "function call arguments (as argument)",
          referencePath.parentPath.get("arguments")
        );
      }
    } else {
      // throw a helpful error message or something :)
    }
  });

  JSXMacro.forEach(referencePath =&gt; {
    if (referencePath.parentPath.type === "JSXOpeningElement") {
      console.log("jsx props", {
        attributes: referencePath.parentPath.get("attributes"),
        children: referencePath.parentPath.parentPath.get("children")
      });
    } else {
      // throw a helpful error message or something :)
    }
  });
}

Next, open up your developer console and check out the console logs. Have fun with that!

> Alternatively, you can just go here

Conclusion

I think there are a LOT of really cool places we can go with this technology. I didn’t spend any time in this newsletter talking about the why behind macros or giving you ideas. I’ll link to some resources for ideas below. The basic idea is if there’s a way that you can pre-compile some of your operations, then you can improve runtime performance/bundle size of your application. In addition, this allows you to do some things at build time when you have access to the file system. The possibilities are really endless and we’re just getting started! Enjoy!


Learn more about ASTs from me:

  • All about macros with babel-plugin-macros šŸŽ£ (talk at ReactJS Utah)
  • Code Transformation and Linting Course on Frontend Masters
  • Code Transformation and Linting Workshop (very rough practice run)
  • Writing custom Babel and ESLint plugins with ASTs (talk at Open West 2017)

Things to not miss:

  • React Europe Talks Day 1
  • React Europe Talks Day 2
  • Stop writing code - Sunil Pai aka @threepointone at @ReactEurope 2018 - See the part where Sunil talks about the origin story of babel-plugin-macros starting at 8m57s. (Also, I love you too Sunil šŸ˜)
  • Pre-evaluate code at build time from Siddharth Kshetrapal.

Some tweets from this last week:

> I’m still figuring out how to find happiness in life, but I’m pretty certain it includes showing compassion for others and yourself. – 20 May 2018

> Are you using React context? Will things bust if someone tries to render your consumer outside of a provider? > > Then DON’T provide a defaultValue! Validate the consumer instead! > > Here’s a little utility I made for that: > > gist.github.com/kentcdodds/515 … – 15 May 2018

Screenshot of the code that's linked in the gist.

> New feature added to react-testing-library: debug() > > Logs a formatted and highlighted version of your rendered container. Enjoy! 🐐 – 15 May 2018

Screenshot of the feature working

> I’m thinking about making daily, short, unedited videos about one thing I learned recently. Would you subscribe to http://kcd.im/youtube for that? – 19 May 2018 (43% voted for šŸ’—šŸ’–šŸ˜šŸ’•šŸ’“šŸ’™šŸ’ššŸ’›šŸ’œšŸ’žšŸ’˜šŸ’Æā€¼ļø sooo…)

This week’s blog post is “Prop Drilling”. It’s the published version of my newsletter from 2 weeks ago. If you thought it was good, go ahead and give it some claps (šŸ‘x50) and a retweet:

screenshot of the tweet

Special thanks to my sponsor Patreons: Hashnode

P.S. If you like this, make sure to subscribe, follow me on twitter, buy me lunch, support me on patreon, and share this with your friends šŸ˜€


šŸ‘‹ Hi! I’m Kent C. Dodds. I work at PayPal as a full stack JavaScript engineer. I represent PayPal on the TC39. I’m actively involved in the open source community. I’m an instructor on egghead.io, Frontend Masters, and Workshop.me. I’m also a Google Developer Expert. I’m happily married and the father of four kids. I like my family, code, JavaScript, and React.

Don't miss what's next. Subscribe to Kent C. Dodds News:
Powered by Buttondown, the easiest way to start and grow your newsletter.