• Best Practices" level="1">Best Practices
    • Create Helper Builders and Checkers" level="2"> Create Helper Builders and Checkers
    • Avoid traversing the AST as much as possible" level="2">Avoid traversing the AST as much as possible
      • Merge visitors whenever possible" level="3">Merge visitors whenever possible
      • Do not traverse when manual lookup will do" level="3">Do not traverse when manual lookup will do
    • Optimizing nested visitors" level="2">Optimizing nested visitors
    • Being aware of nested structures" level="2">Being aware of nested structures
    • Unit Testing" level="2">Unit Testing

Best Practices" class="reference-link">Best Practices

Create Helper Builders and Checkers" class="reference-link"> Create Helper Builders and Checkers

It’s pretty simple to extract certain checks (if a node is a certain type) into their own helper functions as well as extracting out helpers for specific node types.

  1. function isAssignment(node) {
  2. return node && node.operator === opts.operator + "=";
  3. }
  4. function buildAssignment(left, right) {
  5. return t.assignmentExpression("=", left, right);
  6. }

Avoid traversing the AST as much as possible" class="reference-link">Avoid traversing the AST as much as possible

Traversing the AST is expensive, and it’s easy to accidentally traverse the AST more than necessary. This could be thousands if not tens of thousands of extra operations.

Babel optimizes this as much as possible, merging visitors together if it can in order to do everything in a single traversal.

Merge visitors whenever possible" class="reference-link">Merge visitors whenever possible

When writing visitors, it may be tempting to call path.traverse in multiple places where they are logically necessary.

  1. path.traverse({
  2. Identifier(path) {
  3. // ...
  4. }
  5. });
  6. path.traverse({
  7. BinaryExpression(path) {
  8. // ...
  9. }
  10. });

However, it is far better to write these as a single visitor that only gets run once. Otherwise you are traversing the same tree multiple times for no reason.

  1. path.traverse({
  2. Identifier(path) {
  3. // ...
  4. },
  5. BinaryExpression(path) {
  6. // ...
  7. }
  8. });

Do not traverse when manual lookup will do" class="reference-link">Do not traverse when manual lookup will do

It may also be tempting to call path.traverse when looking for a particular node type.

  1. const nestedVisitor = {
  2. Identifier(path) {
  3. // ...
  4. }
  5. };
  6. const MyVisitor = {
  7. FunctionDeclaration(path) {
  8. path.get('params').traverse(nestedVisitor);
  9. }
  10. };

However, if you are looking for something specific and shallow, there is a good chance you can manually lookup the nodes you need without performing a costly traversal.

  1. const MyVisitor = {
  2. FunctionDeclaration(path) {
  3. path.node.params.forEach(function() {
  4. // ...
  5. });
  6. }
  7. };

Optimizing nested visitors" class="reference-link">Optimizing nested visitors

When you are nesting visitors, it might make sense to write them nested in your code.

  1. const MyVisitor = {
  2. FunctionDeclaration(path) {
  3. path.traverse({
  4. Identifier(path) {
  5. // ...
  6. }
  7. });
  8. }
  9. };

However, this creates a new visitor object every time FunctionDeclaration() is called. That can be costly, because Babel does some processing each time a new visitor object is passed in (such as exploding keys containing multiple types, performing validation, and adjusting the object structure). Because Babel stores flags on visitor objects indicating that it’s already performed that processing, it’s better to store the visitor in a variable and pass the same object each time.

  1. const nestedVisitor = {
  2. Identifier(path) {
  3. // ...
  4. }
  5. };
  6. const MyVisitor = {
  7. FunctionDeclaration(path) {
  8. path.traverse(nestedVisitor);
  9. }
  10. };

If you need some state within the nested visitor, like so:

  1. const MyVisitor = {
  2. FunctionDeclaration(path) {
  3. var exampleState = path.node.params[0].name;
  4. path.traverse({
  5. Identifier(path) {
  6. if (path.node.name === exampleState) {
  7. // ...
  8. }
  9. }
  10. });
  11. }
  12. };

You can pass it in as state to the traverse() method and have access to it on this in the visitor.

  1. const nestedVisitor = {
  2. Identifier(path) {
  3. if (path.node.name === this.exampleState) {
  4. // ...
  5. }
  6. }
  7. };
  8. const MyVisitor = {
  9. FunctionDeclaration(path) {
  10. var exampleState = path.node.params[0].name;
  11. path.traverse(nestedVisitor, { exampleState });
  12. }
  13. };

Being aware of nested structures" class="reference-link">Being aware of nested structures

Sometimes when thinking about a given transform, you might forget that the given structure can be nested.

For example, imagine we want to lookup the constructor ClassMethod from the Foo ClassDeclaration.

  1. class Foo {
  2. constructor() {
  3. // ...
  4. }
  5. }
  1. const constructorVisitor = {
  2. ClassMethod(path) {
  3. if (path.node.name === 'constructor') {
  4. // ...
  5. }
  6. }
  7. }
  8. const MyVisitor = {
  9. ClassDeclaration(path) {
  10. if (path.node.id.name === 'Foo') {
  11. path.traverse(constructorVisitor);
  12. }
  13. }
  14. }

We are ignoring the fact that classes can be nested and using the traversal above we will hit a nested constructor as well:

  1. class Foo {
  2. constructor() {
  3. class Bar {
  4. constructor() {
  5. // ...
  6. }
  7. }
  8. }
  9. }

Unit Testing" class="reference-link">Unit Testing

There are a few primary ways to test babel plugins: snapshot tests, AST tests, and exec tests. We’ll use jest for this example because it supports snapshot testing out of the box. The example we’re creating here is hosted in this repo.

First we need a babel plugin, we’ll put this in src/index.js.

  1. module.exports = function testPlugin(babel) {
  2. return {
  3. visitor: {
  4. Identifier(path) {
  5. if (path.node.name === 'foo') {
  6. path.node.name = 'bar';
  7. }
  8. }
  9. }
  10. };
  11. };

Snapshot Tests

Next, install our dependencies with npm install --save-dev babel-core jest, and then we can begin writing our first test: the snapshot. Snapshot tests allow us to visually inspect the output of our babel plugin. We give it an input, tell it to make a snapshot, and it saves it to a file. We check in the snapshots into git. This allows us to see when we’ve affected the output of any of our test cases. It also gives use a diff in pull requests. Of course you could do this with any test framework, but with jest updating the snapshots is as easy as jest -u.

  1. // src/__tests__/index-test.js
  2. const babel = require('babel-core');
  3. const plugin = require('../');
  4. var example = `
  5. var foo = 1;
  6. if (foo) console.log(foo);
  7. `;
  8. it('works', () => {
  9. const {code} = babel.transform(example, {plugins: [plugin]});
  10. expect(code).toMatchSnapshot();
  11. });

This gives us a snapshot file in src/__tests__/__snapshots__/index-test.js.snap.

  1. exports[`test works 1`] = `
  2. "
  3. var bar = 1;
  4. if (bar) console.log(bar);"
  5. `;

If we change ‘bar’ to ‘baz’ in our plugin and run jest again, we get this:

  1. Received value does not match stored snapshot 1.
  2. - Snapshot
  3. + Received
  4. @@ -1,3 +1,3 @@
  5. "
  6. -var bar = 1;
  7. -if (bar) console.log(bar);"
  8. +var baz = 1;
  9. +if (baz) console.log(baz);"

We see how our change to the plugin code affected the output of our plugin, and if the output looks good to us, we can run jest -u to update the snapshot.

AST Tests

In addition to snapshot testing, we can manually inspect the AST. This is a simple but brittle example. For more involved situations you may wish to leverage babel-traverse. It allows you to specify an object with a visitor key, exactly like you use for the plugin itself.

  1. it('contains baz', () => {
  2. const {ast} = babel.transform(example, {plugins: [plugin]});
  3. const program = ast.program;
  4. const declaration = program.body[0].declarations[0];
  5. assert.equal(declaration.id.name, 'baz');
  6. // or babelTraverse(program, {visitor: ...})
  7. });

Exec Tests

Here we’ll be transforming the code, and then evaluating that it behaves correctly. Note that we’re not using assert in the test. This ensures that if our plugin does weird stuff like removing the assert line by accident, the test will still fail.

  1. it('foo is an alias to baz', () => {
  2. var input = `
  3. var foo = 1;
  4. // test that foo was renamed to baz
  5. var res = baz;
  6. `;
  7. var {code} = babel.transform(input, {plugins: [plugin]});
  8. var f = new Function(`
  9. ${code};
  10. return res;
  11. `);
  12. var res = f();
  13. assert(res === 1, 'res is 1');
  14. });

Babel core uses a similar approach to snapshot and exec tests.

babel-plugin-tester

This package makes testing plugins easier. If you’re familiar with ESLint’s RuleTester this should be familiar. You can look at the docs to get a full sense of what’s possible, but here’s a simple example:

  1. import pluginTester from 'babel-plugin-tester';
  2. import identifierReversePlugin from '../identifier-reverse-plugin';
  3. pluginTester({
  4. plugin: identifierReversePlugin,
  5. fixtures: path.join(__dirname, '__fixtures__'),
  6. tests: {
  7. 'does not change code with no identifiers': '"hello";',
  8. 'changes this code': {
  9. code: 'var hello = "hi";',
  10. output: 'var olleh = "hi";',
  11. },
  12. 'using fixtures files': {
  13. fixture: 'changed.js',
  14. outputFixture: 'changed-output.js',
  15. },
  16. 'using jest snapshots': {
  17. code: `
  18. function sayHi(person) {
  19. return 'Hello ' + person + '!'
  20. }
  21. `,
  22. snapshot: true,
  23. },
  24. },
  25. });