preface

For front-end development, Babel is certainly familiar enough to be used in your work. In addition to being used as a tool to transform ES6 and JSX, I feel that Babel’s plug-in mechanism, based on abstract syntax trees, offers us more possibilities. There is a lot of good documentation online about Babel concepts and plug-ins. Detailed analysis is recommended in the official Babel plug-in manual. There are some things you should know before you start developing plug-ins that you can skip if you’re already familiar with them.

Abstract Syntax Tree (AST)

Babel using a ESTree based and modified the AST, its kernel documentation can be in [here] (https://github. com/Babel/Babel/blob/master/doc/AST/spec. Md) found. It should be clearer to look directly at the example:

function square(n) {
  return n * n;
}
Copy the code

Corresponding AST object (object format provided by Babel)

{
  // Code block category, function declaration
  type: "FunctionDeclaration".// Variable identifier
  id: {
    type: "Identifier".// Variable name
    name: "square"
  },
  / / parameters
  params: [{
    type: "Identifier".name: "n"}]./ / the function body
  body: {
     / / block statements
    type: "BlockStatement".body: [{
       / / return statement
      type: "ReturnStatement".argument: {
        // A binary expression
        type: "BinaryExpression"./ / operators
        operator: "*".left: {
          type: "Identifier".name: "n"
        },
        right: {
          type: "Identifier".name: "n"}}}]}}Copy the code

Each layer is called a Node, and the JS object corresponding to a complete AST may have many nodes, depending on the situation. Babel returns each node as an interface. These include properties as shown in the code above, such as type, start, end, LOc and other general properties and private properties corresponding to specific types. The processing of our later plug-ins is also handled according to different types.

Don’t be bothered by the sheer size of the JS object. It’s not realistic to parse the AST and remember the different types as defined by Babel every time. This should be left to the computer. We can use the AST Explorer to convert object code into syntax tree objects and combine the AST Node Types to view specific properties.

Processing steps for Babel

Babel’s three main processing steps are: parse, transform and generate. We don’t want to describe the process in detail, but read the official manual. It is important to note that the Babel plug-in comes into play during the transformation process, where the parsed syntax tree object is processed for its own purposes before the code generation step. So learn more about transformations.

The code generation step converts the final AST (after a series of transformations) into string code, and also creates source maps for easy debugging.

Code generation works by going depth-first through the AST and then building a string that represents the transformed code. When you convert, you do recursive tree traversal.

conversion

Visitor

When it comes to transformation, that’s when the plug-in comes into play, but how to get into that process, Babel provides us with a specification of that Visitor. We can define our access logic through the Visitor. It’s going to look something like this

const MyVisitor = {
  // This corresponds to the node type above. All nodes whose type is Identifier will enter this method
  Identifier() {
    console.log("Called!"); }};// Take this method as an example
function square(n) {
  return n * n;
} 
// will be called four times, because
// The function name is square
/ / parameter n
// The two n's in the function body are identifiers
path.traverse(MyVisitor); 
// So output four
Called!
Called!
Called!
Called!
Copy the code

Because of the depth-first traversal calculation method, after arriving at a leaf node, it is found that there are no descendant nodes, so it is necessary to trace up to go back to the upper level to continue traversing the next child node, so each node will be visited twice. If not specified, the calls occur when the node is entered, although the visitor method can also be called on exit.

const MyVisitor = {
  Identifier: {
    enter() {
      console.log("Entered!");
    },
    exit() {
      console.log("Exited!"); }}};Copy the code

Here are some other tips:

Can use | in the method name to match a variety of different type, use the same processing function.

const MyVisitor = {
  "ExportNamedDeclaration|Flow"(path) {}
};
Copy the code

You can also use aliases in visitors (as defined by babel-types) such as Function is FunctionDeclaration, FunctionExpression, ArrowFunctionExpression, Alias of ObjectMethod and ObjectMethod, which can be used to match all of the above types of Type

const MyVisitor = {
  Function(path) {}
};
Copy the code

Paths

An AST usually has many nodes, so how do the nodes relate directly to each other? We can represent relationships between nodes with a large mutable object that can be manipulated and accessed, or we can simplify things with Paths. Path is an object that represents the connection between two nodes. It’s a little bit clearer to look at examples.


{
  type: "FunctionDeclaration".id: {
    type: "Identifier".name: "square"},... }Copy the code

The child Identifier, expressed as a Path, looks like this:

{
  "parent": {
    "type": "FunctionDeclaration"."id": {... },... },"node": {
    "type": "Identifier"."name": "square"}}Copy the code

When you pass a visitor to an Identifier() member method, you are actually accessing a path, not a node. In this way, you are dealing with the responsive representation of the node rather than the node itself.

Writing a plug-in

The front are some essential knowledge points, this article is just some relatively important points mentioned. Detailed or want to see the development manual. Personally, there are three steps to developing a plug-in:

  1. Analyze the source file abstract syntax tree AST
  2. Analyze the object file abstract syntax tree
  3. Build Visitor 3.1 Determine access conditions 3.2 Determine conversion logic

There are three main steps to a plug-in, but the first two are very important. 3.1 and 3.2 depend on the results of 1 and 2 respectively. Only after a clear understanding of AST structure can we have a clear target and get twice the result with half the effort. For example, the following code:

var func = (a)= >{
    console.log(this.b)
}; 
Copy the code

The goal is to convert the arrow function into a normal function declaration (this is just a detailed conversion of this format, not to mention the rest). As follows:

var _this = this;
var func = function () {
    console.log(_this.b);
};
Copy the code

Source file syntax tree

Here’s a look at this simple function declaration, as defined above, but it’s recommended that the AST Explorer see our syntax tree clearly. Only useful information is captured here:

        "init": {
              "type": "ArrowFunctionExpression"./ *... Other information.... * /
              "id": null./ / parameter
              "params": []."body": {
                // Function body,this part
                "arguments": [{"type": "MemberExpression"."object": {
                             / / this expression
                            "type": "ThisExpression",},"property": {
                             / / b attributes
                            "type": "Identifier"."name": "b"}}]}}Copy the code

The only thing we want to transform is the ArrowFunctionExpression that is the arrow function and this expression ThisExpression part, leaving everything else untouched for now. So the function name in our visitor includes ArrowFunctionExpression and ThisExpression.

// The key of the method in the visitor corresponds to the node type we are processing
const visitor = {
    // Handle this expression
    ThisExpression(path){
        // Convert this to _this
    },
    // Handle the arrow function.
    ArrowFunctionExpression(path){
       // Convert to normal FunctionExpression}}Copy the code

Object file syntax tree

In the same way, the syntax tree object looks like this: The syntax tree is too long, so let’s look at the changes

    // The transformed body consists of an array of two elements. The two variables are declared to be statistical
    "body": [
      //var _this = this; structure
      {
        "type": "VariableDeclaration"."kind": "var"."declarations": [{"type": "VariableDeclarator".//left is the identifier of _this
            "id": {
              "type": "Identifier"."name": "_this"
            },
            //right is this expression
            "init": {
              "type": "ThisExpression"
              / * * * * * /}},// var func = function (b) {
      // console.log(_this.b);
      / /}; Structure only looks at the key
      {
        "type": "VariableDeclaration"."kind": "var"."declarations": [{/ * * * * * omit * * * * * * * /
            "arguments": [{"type": "MemberExpression".// The converted _this.b
                          "object": {
                            "type": "Identifier"."name": "_this"
                          },
                          "property": {
                            "type": "Identifier"."name": "b"}]}}]Copy the code

After comparison, it is determined that our operation should be to replace ArrowFunctionExpression with FunctionExpression, bind this when this expression is encountered, and convert it. The API provided by Path is used to do things like add and replace:

  • Replace replaceWith (targetObj)
  • FindParent () finds the parent that satisfies the condition
  • InsertBefore Inserts siblings for more please consult the documentation, which lists only the methods we use.

Tectonic node

ToFunctionExpression is an API that I never found… Maybe I’m not looking for FunctionExpression in the right place. I have no choice but to look it up in Babel source code:

//@src /babel/packages/babel-types/src/definitions/core.js
defineType("FunctionExpression", {
  inherits: "FunctionDeclaration".//....
}
// Find FunctionDeclaration
defineType("FunctionDeclaration", {
  Id,params,body..
  builder: ["id"."params"."body"."generator"."async"].visitor: ["id"."params"."body"."returnType"."typeParameters"]
  //....  
}

Copy the code

If there is a clear document, please do not hesitate to comment. Now this is easy.

Later, I looked for it and finally found the corresponding documentportal

To improve the Visitor

const Visitor = {
    / / this expression
    ThisExpression(path){
        Build var _this = this
        let node = t.VariableDeclaration(
            'var',
            [
                t.VariableDeclarator(
                    t.Identifier('_this'),
                    t.Identifier('this'))),// Build the _this identifier
        str = t.Identifier('_this'),
        // Find the parent node of the variable declaration
        // This is for example only, but there are a lot of cases to consider for real conversion
        parentPath = path.findParent((path) = > path.isVariableDeclaration())
        // The condition is met
        if(parentPath){
            / / insert
            parentPath.insertBefore(node)
            path.replaceWith(
                str
            )
        }else{
            return}},// Handle the arrow function.
    ArrowFunctionExpression(path){
        var node = path.node
        // create a t. unctionExpression node and replace the original path
        path.replaceWith(t.FunctionExpression(
            node.id,
            node.params,
            node.body
          ))
    }
}     
Copy the code

That’s the end of the main visitor, of course, if it’s a plug-in

// babel-types is passed as an argument when the plug-in is called by Babel
export default function({ types: t }) {
  return {
    visitor:Visitor
  }
Copy the code

For local debugging, you can introduce babel-core and babel-types respectively

var babel = require('babel-core');
var t = require('babel-types');
var code = `var func = ()=>{ console.log(this.b) }; `
const result = babel.transform(code, {
	plugins: [{
	  // Visitor in front
		visitor: Visitor
	}]
});  
// Output the converted code
/** * var _this = this; * var func = function () { * console.log(_this.b); *}; * /
console.log(result.code);  
Copy the code

conclusion

Refer to the article

Babel for ES6? And Beyond!

I thought I understood the principles of Babel and the plugin mechanism, but I didn’t realize it was so hard to write a small demo. I’m still not familiar with the relevant API, and I don’t know how to build nodes. This article is a simple summary of the plug-in manual, the idea of their own implementation is summarized. Throw a brick to attract jade, common progress, in addition, I hope to have a little help to the students in need. See more on my blog