Introduction to the

Babel is a javascript compiler whose main job is to retrofit ECMAScript 2015+ code to the current browser or environment. The immediate benefit of this is that you can write code with a higher version of standard syntax without too many environmental compatibility considerations.

Babel provides a plug-in system that allows anyone to write plug-ins based on Babel to implement custom syntax conversions, which is a boon for developers.

And at the base of all this is a concept you need to know: the Syntax Tree, or AST.

The AST represents your code. Editing an AST is the same as editing your code. Traditional compilers have structures that do the same thing called concrete Syntax parsing trees (CST), and the AST is a simplified version of CST.

How do I convert code using Babel

Here is a simple conversion example:

import { parse } from '@babel/parser';
import traverse from '@babel/traverse';
import generate from '@babel/generator';

const code = 'const n = 1';

// parse the code -> ast
const ast = parse(code);

// transform the ast
traverse(ast, {
  enter(path) {
    // in this example change all the variable `n` to `x`
    if (path.isIdentifier({ name: 'n' })) {
      path.node.name = 'x'; }}});// generate code <- ast
const output = generate(ast, code);
console.log(output.code); // 'const x = 1; '
Copy the code

Parse -> Transform -> generate, three clear steps to complete the code transformation operation.

You can do this by installing @babel/core. @babel/ Parser, @babel/traverse, @babel/ Generator are all dependencies of @babel/core, so just install @babel/core.

The transformation is implemented through plug-ins

In addition to the above method, a more common way to do this is through plug-ins:

import babel from '@babel/core';

const code = 'const n = 1';

const output = babel.transformSync(code, {
  plugins: [
    // your first babel plugin 😎😎
    function myCustomPlugin() {
      return {
        visitor: {
          Identifier(path) {
            // in this example change all the variable `n` to `x`
            if (path.isIdentifier({ name: 'n' })) {
              path.node.name = 'x'; ,}}}}; },]});console.log(output.code); // 'const x = 1; '
Copy the code

Extract the myCustomPlugin function into a separate file, export it and publish it as an NPM package, and you can proudly say THAT I published a Babel plug-in, 😁.

How does Babel’s AST work?

1. Want to do some transformational tasks

We do a code obfuscation conversion, which inverts the variable and function names and adds up the strings to make the code less readable.

At the same time to maintain the original function, the source code is as follows:

function greet(name) {
  return 'Hello ' + name;
}

console.log(greet('lumin'));
Copy the code

Converted to:

function teerg(eman) {
  return 'H' + 'e' + 'l' + 'l' + 'o' + ' ' + eman;
}

console.log(teerg('l' + 'u' + 'm' + 'i' + 'n'));
Copy the code

Here again, we need to leave the console.log function unchanged to keep it functional.

2. How is source code represented as AST

You can use the babel-ast-Explorer tool to view the AST tree, which looks like this:

Now we need to know two key words:

  • IdentifierUsed to recordThe function nameandThe variable name;
  • StringLiteralUsed to recordstring;

3. What about AST after transformation

Using the babel-ast-Explorer tool, we can see the transformed AST structure:

4. coding now !

Our code would look like this:

function myCustomPlugin() {
  return {
    visitor: {
      Identifier(path) {
        // ...,}}}; }Copy the code

The AST traversal uses the visitor pattern.

In the traversal phase, Babel uses a depth-first search to access each of the AST’s nodes. You can specify a callback method in the visitor that will be called when the current node is traversed.

On the visitor object, specify a node name to get the desired callback:

function myCustomPlugin() {
  return {
    visitor: {
      Identifier(path) {
        console.log('identifier');
      },
      StringLiteral(path) {
        console.log('string literal'); ,}}}; }Copy the code

After running, we should get the following log output:

identifier
identifier
string literal
identifier
identifier
identifier
identifier
string literal
Copy the code

Before continuing, let’s look at the path parameter to Identifer(path) {}.

Path represents the object connected between two nodes. It contains properties such as scope and context, and also provides methods such as insertBefore, replaceWith, and remove to add, update, move, and delete nodes.

5. Convert variable names

Using the babel-ast-Explorer tool, we can see that the variable name is stored in the name of Identifer, so we can simply reverse the name and reassign it:

Identifier(path) {
  path.node.name = path.node.name.split(' ').reverse().join(' ');
}
Copy the code

After running, we have the following code:

function teerg(eman) {
  return 'Hello ' + eman;
}

elosnoc.gol(teerg('lumin'));
Copy the code

Obviously we don’t want console.log to change, so how do we keep it the same?

Let’s go back to the AST representation of console in the source code:

You can see that console.log is part of MemberExpression, console is object, and log is property.

So we do some pre-validation:

Identifier(path) {
  if(! ( path.parentPath.isMemberExpression() && path.parentPath.get('object').isIdentifier({ name: 'console' }) &&
      path.parentPath.get('property').isIdentifier({ name: 'log' })
    )
  ) { path.node.name = path.node.name.split(' ').reverse().join(' '); }}Copy the code

Results:

function teerg(eman) {
  return 'Hello ' + eman;
}

console.log(teerg('lumin'));
Copy the code

Ok, that looks good.

Q&A

Q: How do we know if a method is isMemberExpression or isIdentifier?

A: OK, all node types of Babel are defined by @babel/types and matched by isXxxx validation function. For example, the anyTypeAnnotation function has a corresponding isAnyTypeAnnotation validator. If you want to see more detailed validators, check out the Babel source code section.

6. Convert strings

The next thing you do is generate a nested BinaryExpression from StringLiteral.

To create an AST node, you can use a generic function from @babel/types, or the same from @babel/core:

// the ❌ code is not complete
StringLiteral(path) {
  const newNode = path.node.value
    .split(' ')
    .map(c= > babel.types.stringLiteral(c))
    .reduce((prev, curr) = > {
      return babel.types.binaryExpression('+', prev, curr);
    });

  path.replaceWith(newNode);
}
Copy the code

We break path.node.value into byte arrays and iterate to create StringLiteral, then concatenate StringLiteral with BinaryExpression. Finally, replace the current StringLiteral with the new AST node we created.

Everything seems fine, but we get an error:

RangeError: Maximum call stack size exceeded
Copy the code

Why 🤷?

A: Because after we create StringLiteral, Babel will visit it and end up executing an infinite loop causing stack overflow.

We can tell Babel to skip traversal of the current node by path.skip() :

// ✅ modified code
StringLiteral(path) {
  const newNode = path.node.value
    .split(' ')
    .map(c= > babel.types.stringLiteral(c))
    .reduce((prev, curr) = > {
      return babel.types.binaryExpression('+', prev, curr);
    });
  path.replaceWith(newNode);
  path.skip();
}
Copy the code

7. Complete code at the end

const babel = require('@babel/core');
const code = ` function greet(name) { return 'Hello ' + name; } console.log(greet('lumin')); `;
const output = babel.transformSync(code, {
  plugins: [
    function myCustomPlugin() {
      return {
        visitor: {
          StringLiteral(path) {
            const concat = path.node.value
              .split(' ')
              .map(c= > babel.types.stringLiteral(c))
              .reduce((prev, curr) = > {
                return babel.types.binaryExpression('+', prev, curr);
              });
            path.replaceWith(concat);
            path.skip();
          },
          Identifier(path) {
            if (
              !(
                path.parentPath.isMemberExpression() &&
                path.parentPath.get('object').isIdentifier({ name: 'console' }) &&
                path.parentPath.get('property').isIdentifier({ name: 'log' })
              )
            ) {
              path.node.name = path.node.name.split(' ').reverse().join(' '); ,}}}}; },]});console.log(output.code);
Copy the code

Ok, that’s all!

Deep exploration

If you want more, the Babel repository provides more examples of how to convert code, which is a good place to do it.

Look in the babel-plugin-transform-* or babel-plugin-proposal-* directory at github.com/babel/babel You can see the Nullish coalescing operator:?? And the Optional chaining operator:?. Such as proposed phase conversion source.

There is also a plugin manual for Babel, which is highly recommended.

Read more:

> github.com/jamiebuilds…

> lihautan.com/step-by-ste…

> zh.wikipedia.org/wiki/ visitor pattern

> zh.wikipedia.org/wiki/ depth-first search

> babeljs.io/docs/en/