Skip to content

Latest commit

History

History
346 lines (258 loc) 路 10 KB

author.md

File metadata and controls

346 lines (258 loc) 路 10 KB

babel-plugin-macros Usage for macros authors

See also: the user docs.

Is this your first time working with ASTs? Here are some resources:

Writing a macro

You might appreciate this example repo which shows how to write and use macros in a create-react-app application.

A macro is a JavaScript module that exports a function. Here's a simple example:

const {createMacro} = require('babel-plugin-macros')

// `createMacro` is simply a function that ensures your macro is only
// called in the context of a babel transpilation and will throw an
// error with a helpful message if someone does not have babel-plugin-macros
// configured correctly
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-plugin-macros` module.
  // do whatever you like to the AST paths you find in `references`
  // read more below...
}

It can be published to the npm registry (for generic macros, like a css-in-js library) or used locally (for domain-specific macros, like handling some special case for your company's localization efforts).

Before you write a custom macro, you might consider whether babel-plugin-preval help you do what you want as it's pretty powerful.

There are two parts to the babel-plugin-macros API:

  1. The filename convention
  2. The function you export

Filename

The way that babel-plugin-macros determines whether to run a macro is based on the source string of the import or require statement. It must match this regex: /[./]macro(\.c?js)?$/ for example:

matches:

'my.macro'
'my.macro.js'
'my.macro.cjs'
'my/macro'
'my/macro.js'
'my/macro.cjs'

does not match:

'my-macro'
'my.macro.is-sweet'
'my/macro/rocks'

So long as your file can be required at a matching path, you're good. So you could put it in: my/macro/index.js and people would: require('my/macro') which would work fine.

If you're going to publish this to npm, the most ergonomic thing would be to name it something that ends in .macro. If it's part of a larger package, then calling the file macro.js or placing it in macro/index.js is a great way to go as well. Then people could do:

import Nice from 'nice.macro'
// or
import Sweet from 'sweet/macro'

In addition, please publish your macro with the keyword of babel-plugin-macros (note the "s"). That way folks can easily find macros by searching for the babel-plugin-macros keyword on npm. In addition, and you can add this badge to the top of your README:

Babel Macro

[![Babel Macro](https://img.shields.io/badge/babel--macro-%F0%9F%8E%A3-f5da55.svg?style=flat-square)](https://github.com/kentcdodds/babel-plugin-macros)

Function API

The macro you create should export a function. That function accepts a single parameter which is an object with the following properties:

state

The state of the file being traversed. It's the second argument you receive in a visitor function in a normal babel plugin.

babel

This is the same thing you get as an argument to normal babel plugins. It is also the same thing you get if you require('babel-core').

references

This is an object that contains arrays of all the references to things imported from macro keyed based on the name of the import. The items in each array are the paths to the references.

Some examples:
import MyMacro from './my.macro'

MyMacro(
  {someOption: true},
  `
  some stuff
`,
)

// references: { default: [BabelPath] }
import {foo as FooMacro} from './my.macro'

FooMacro(
  {someOption: true},
  `
  some stuff
`,
)

// references: { foo: [BabelPath] }
import {foo as FooMacro} from './my.macro'

// no usage...

// references: {}

From here, it's just a matter of doing stuff with the BabelPaths that you're given. For that check out the babel handbook.

One other thing to note is that after your macro has run, babel-plugin-macros will remove the import/require statement for you.

source

This is a string used as import declaration's source - i.e. './my.macro'.

config

There is a feature that allows users to configure your macro.

To specify that your plugin is configurable, you pass a configName to createMacro.

A configuration is created from data combined from two sources: We use cosmiconfig to read a babel-plugin-macros configuration which can be located in any of the following files up the directories from the importing file:

  • .babel-plugin-macrosrc
  • .babel-plugin-macrosrc.json
  • .babel-plugin-macrosrc.yaml
  • .babel-plugin-macrosrc.yml
  • .babel-plugin-macrosrc.js
  • babel-plugin-macros.config.js
  • babelMacros in package.json

The content of the config will be merged with the content of the babel macros plugin options. Config options take priority.

All together specifying and using the config might look like this:

// .babel-plugin-macros.config.js
module.exports = {
  taggedTranslations: {locale: 'en_US'},
}

// .babel.config.js
module.exports = {
  plugins: [
    [
      "macros",
      {
        taggedTranslations: { locale: "en_GB" },
      },
    ],
  ],
}


// taggedTranslations.macro.js
const {createMacro} = require('babel-plugin-macros')
module.exports = createMacro(taggedTranslationsMacro, {
  configName: 'taggedTranslations',
})
function taggedTranslationsMacro({references, state, babel, config}) {
  const {locale = 'en'} = config
}

Note that in the above example if both files were specified, the final locale value would be en_US, since that is the value in the plugin config file.

Keeping imports

As said before, babel-plugin-macros automatically removes an import statement of macro. If you want to keep it because you have other plugins processing macros, return { keepImports: true } from your macro:

const {createMacro} = require('babel-plugin-macros')

module.exports = createMacro(taggedTranslationsMacro)

function taggedTranslationsMacro({references, state, babel}) {
  // process node from references

  return {
    keepImports: true,
  }
}

Throwing Helpful Errors

Debugging stuff that transpiles your code is the worst, especially for beginners. That's why it's important that you make assertions, and catch errors to throw more meaningful errors with helpful information for the developer to know what to do to resolve the issue.

In an effort to make this easier for you, babel-plugin-macros will wrap the invocation of your plugin in a try/catch and throw as helpful an error message as possible for you.

To make it even better, you can throw your own with more context. For example:

const {createMacro, MacroError} = require('babel-plugin-macros')

module.exports = createMacro(myMacro)

function myMacro({references, state, babel}) {
  // something unexpected happens:
  throw new MacroError(
    'Some helpful and contextual message. Learn more: ' +
      'https://github.com/your-org/your-repo/blob/master/docs/errors.md#learn-more-about-eror-title',
  )
}

Testing your macro

The best way to test your macro is using babel-plugin-tester:

import pluginTester from 'babel-plugin-tester'
import plugin from 'babel-plugin-macros'

pluginTester({
  plugin,
  snapshot: true,
  babelOptions: {filename: __filename},
  tests: [
    `
      import MyMacro from '../my.macro'

      MyMacro({someOption: true}, \`
        some stuff
      \`)
    `,
  ],
})

There is currently no way to get code coverage for your macro this way however. If you want code coverage, you'll have to call your macro yourself. Contributions to improve this experience are definitely welcome!

Async logic

Unfortunately, babel plugins are synchronous so you can't do anything asynchronous with babel-plugin-macros. However, you can cheat a bit by running child_process's spawnSync to synchronously execute a file. It's definitely a hack and is not great for performance, but in most cases it's fast enough鈩笍.

Luckily, @Zemnmez created do-sync which makes doing this much more straightforward:

const {doSync} = require('do-sync')
const {createMacro, MacroError} = require('babel-plugin-macros')

module.exports = createMacro(myMacro)

const getTheFlowers = doSync(async (arg1, arg2) => {
  const dep = require('some-dependency')
  const flowers = await dep(arg1, arg2.stuff)
  return flowers
})

function myMacro({references, state, babel}) {
  const flowers = getTheFlowers('...', {stuff: '...'})
  // ... more sync stuff
}