Getting started with Babel

Babel downloads JavaScript code written in the latest standards into versions that are available everywhere today.This process is called translation.

For example, Babel can incorporate the new ES2015 arrow function syntax:

const square = n => n * n;

Translate to:

const square = function square(n) {
  return n * n;
};

Babel does more than that, however, support syntax extensions, JSX syntax like React, and Flow Syntax for static type checking.

More importantly, everything about Babel is simple plug-ins. Anyone can create their own plug-ins and do anything with Babel's full power.

Further, Babel itself is broken down into several core modules that anyone can use to create the next generation of JavaScript tools.

Many people have already done so, and very large and diverse ecosystems have emerged around Babel.In this book, you'll learn how to use Babel's built-in tools and some very useful things from the community.

Basic Use

Install Babel

Since the JavaScript community does not have uniform building tools, frameworks, platforms, and so on, Babel formally integrates support for all mainstream tools.From Gulp to Browserify, Ember to Meteor, Babel has formal integration support regardless of your environment settings.

The main purpose of this manual is to introduce the installation of Babel's built-in methods, but you can visit the interactive installation page to see other integration methods.

First, create a new demo to facilitate the demo:

mkdir -p babel-demo && cd babel-demo
npm init -y
git init
// .gitignore
node_modules
// src/index.js
const square = n => n * n;

@babel/cli

@babel/cli is a simple way to compile files using Babel from the command line.

Install it globally to learn the basics.@babel/cli depends on @babel/core

npm install --save-dev @babel/cli @babel/core

We can compile src/index.js:

npx babel src/index.js

This will output the compiled results directly to the terminal:

babel-demo git:(master) ✗ npx babel src/index.js
// src/index.js
const square = n => n * n;

Use --out-file or -o to write the results to the specified file.

npx babel src/index.js -o build/index.js
// build/index.js
// src/index.js
const square = n => n * n;

If we want to compile a directory as a whole into a new directory, we can use --out-dir or -d..

npx babel src -d build

@babel/register

Create src/Index.jsFile.

// src/index.js
const square = n => n * n;
console.log(square(2));

Install @babel/register first.

npm install --save-dev @babel/register

Next, create src/Register.jsFile and add the following code:

// src/register.js
require("@babel/register");
require("./index.js");

This allows you to register babel with Node's module system, and then all require s will translate the file by default when they end in.Es6,.Es,.Jsx,.Mjs,.Js.This approach is not appropriate for production environments and is appropriate for scripting.

Now we can use node src/Register.jsCome and run.

babel-demo git:(master) ✗ node src/register.js
4

@babel/node

If you're running code with the node CLI, the easiest way to integrate Babel is to use the @babel/node CLI, which is an alternative to the node CLI.This approach is not appropriate for production environments and is appropriate for scripting.

Use babel-node instead of node to run all the code.

If you use npm scripts, you only need to do this:

  {
    "scripts": {
-     "script-name": "node script.js"
+     "script-name": "babel-node script.js"
    }
  }

It's fine too:

npx babel-node script.js

@babel/core

If you need to use Babel programmatically, you can use the @babel/core package.

Install @babel/core first.

$ npm install @babel/core
var babel = require("@babel/core");

JavaScript code as strings can be used directlyBabel.transformTo compile.

babel.transform("code();", options);
// => { code, map, ast }

If it's a file, you can use an asynchronous api:

babel.transformFile("filename.js", options, function(err, result) {
  result; // => { code, map, ast }
});

Or synchronization api:

babel.transformFileSync("filename.js", options);
// => { code, map, ast }

If you already have a Babel AST (Abstract Grammar Tree), you can convert directly from AST.

babel.transformFromAst(ast, code, options);
// => { code, map, ast }

options reference http://babeljs.io/docs/usage/...

Configure Babel

You may have noticed that so far by running Babel himself we have not been able to "translate" the code, but simply copy it from one place to another.

That's because we haven't told Babel what to do yet, and by default it does nothing.

You can tell Babel what to do by installing plugins or presets.

.babelrc

You can use a configuration file to tell babel how to translate code.All you need to do is create the.babelrc file at the root of the project:

{
  "presets": [],
  "plugins": []
}

Options can be passed to Babel in other ways, but the.babelrc file is the Convention and the best way.

@babel/preset-env

@babel/preset-es2015 is obsolete. We use @babel/preset-env to convert all ECMAScript 2015+ codes into ES5 codes:

npm install --save-dev @babel/preset-env

We modify.babelrc to include this preset.When configuring plugin, preset in.babelrc, @babel/preset-env = preset-env, and so on.

{
  "presets": [
    "@babel/preset-env"
  ],
  "plugins": []
}

package.json

"scripts": {
    "babel-script": "babel-node src/index.js",
    "build": "babel src/index.js "
  },

Run npx babel src/index.js

babel-demo git:(master) ✗ npx babel src/index.js 
"use strict";

// src/index.js
var square = function square(n) {
  return n * n;
};

console.log(square(2));

Code compatibility using @babel/preset-env: three fields, useBuiltIns, target, core-js, need to be considered.

@babel/preset-env has many configuration options, see Official Web Code compatibility occurs when useBuiltIns = "usage" | "entry".

False: default value of useBuiltIns, not compatible.

usage: Recommended.Converts the code to runnable code in the target environment specified by the target, and what new features babel used by the original code is automatically compatible with these new features.

.babelrc

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "useBuiltIns": "entry",
        "targets": {
          "node": "6"
        },
        "corejs": 3
      }
    ]
  ]
}

src/index.js

// src/index.js
const a = async n => {
  await Promise.resolve(123)
}

Run NPX Babel src-d build:

// build/index.js
"use strict";
const a = /*#__PURE__*/function () {
  var _ref = _asyncToGenerator(function* (n) {
    yield Promise.resolve(123);
  });

  return function a(_x) {
    return _ref.apply(this, arguments);
  };
}();

Entry: With this option, core-js and regenerator-runtime/runtime need to be entered manually in the entry JS file import/require, and babel will convert the statements of this import/require package into compatible statements corresponding to the target environment specified by target (which features are missing from an environment that fully supports core-js, for which packages import/require corresponds).

// src/index.js
import "core-js/stable";
import "regenerator-runtime/runtime";
{
  "presets": [
    [
      "@babel/preset-env",
      {
        "useBuiltIns": "entry",
        "targets": {
          "chrome": "72"
        },
        "corejs": 3
      }
    ]
  ]
}

Run NPX Babel src-d build:

// build/index.js
"use strict";

require("core-js/modules/es.array.unscopables.flat");

require("core-js/modules/es.array.unscopables.flat-map");

require("core-js/modules/es.math.hypot");

require("core-js/modules/es.object.from-entries");

require("core-js/modules/web.immediate");

Visit the official website link try-it-out Can test the translation results for each option very intuitively.

@babel/preset-react

WIP: Not used, update later.

Setting up React is as easy.Just install this preset:

$ npm install --save-dev @babel/preset-react

Then add in the.babelrc file:

  {
    "presets": [
      "es2015",
+     "react"
    ],
    "plugins": []
  }

@babel/preset-stage-x

@babel/preset-stage-x is obsolete.Not introduced.

polyfill with Babel

Even if you've compiled your code with Babel, that's not all.

@babel/polyfill

Abandoned, feature compatibility with @babel/preset-env is recommended.

@babel/runtime

@babel/preset-env is recommended for feature compatibility.

Configure Babel (Advanced)

WIP: Not very common for most people, so have time to study again.

Implement Babel Plugin

Here's how to create a Babel plug-in and more.

Basics

Abstract Grammar Tree (ASTs)

Each step in this process involves creating or manipulating an abstract grammar tree (AST).

function square(n) {
  return n * n;
}

AST Explorer Allows you to have a better perceptual understanding of AST nodes.

This program can be represented as a tree:

- FunctionDeclaration:
  - id:
    - Identifier:
      - name: square
  - params [1]
    - Identifier
      - name: n
  - body:
    - BlockStatement
      - body [1]
        - ReturnStatement
          - argument
            - BinaryExpression
              - operator: *
              - left
                - Identifier
                  - name: n
              - right
                - Identifier
                  - name: n

Or a JavaScript Object like this:

{
  type: "FunctionDeclaration",
  id: {
    type: "Identifier",
    name: "square"
  },
  params: [{
    type: "Identifier",
    name: "n"
  }],
  body: {
    type: "BlockStatement",
    body: [{
      type: "ReturnStatement",
      argument: {
        type: "BinaryExpression",
        operator: "*",
        left: {
          type: "Identifier",
          name: "n"
        },
        right: {
          type: "Identifier",
          name: "n"
        }
      }
    }]
  }
}

You will notice that each layer of AST has the same structure:

{
  type: "FunctionDeclaration",
  id: {...},
  params: [...],
  body: {...}
}
{
  type: "Identifier",
  name: ...
}
{
  type: "BinaryExpression",
  operator: ...,
  left: {...},
  right: {...}
}

Note: Some attributes have been removed for simplification purposes

Each of these layers is also known as Node.An AST can be made up of a single node or hundreds or thousands of nodes.Together, they describe the program syntax used for static analysis.

Each node has an Interface as shown below:

interface Node {
  type: string;
}

The type field as a string represents the type of node (for example, "FunctionDeclaration", "Identifier", or "BinaryExpression").Each type of node defines additional properties that further describe the node type.

Babel also generates additional attributes for each node to describe its location in the original code.

{
  type: ...,
  start: 0,
  end: 38,
  loc: {
    start: {
      line: 1,
      column: 0
    },
    end: {
      line: 3,
      column: 1
    }
  },
  ...
}

Each node will have start, end, loc attributes.

Processing steps for Babel

The three main steps Babel takes are parse, transform, and generate..

analysis

Parse the step to receive code and output AST.

Transformation

The conversion step receives the AST and traverses it, adding, updating, and removing nodes in the process.This is the most complex process in Babel or other compilers, and it is also the part of the plug-in that will get involved.

generate

The code generation step converts the final (converted) AST into a string of code, along with creating the source maps.

ergodic

To convert AST you need to do a recursive tree traversal.

For example, we have a FunctionDeclaration type.It has several properties: id, params, and body, each with some embedded nodes.

{
  type: "FunctionDeclaration",
  id: {
    type: "Identifier",
    name: "square"
  },
  params: [{
    type: "Identifier",
    name: "n"
  }],
  body: {
    type: "BlockStatement",
    body: [{
      type: "ReturnStatement",
      argument: {
        type: "BinaryExpression",
        operator: "*",
        left: {
          type: "Identifier",
          name: "n"
        },
        right: {
          type: "Identifier",
          name: "n"
        }
      }
    }]
  }
}

So we start with FunctionDeclaration and we know its internal properties (i.e., id, params, body), so we access each one in turn and their child nodes.

Then we come to id, which is an Identifier.Identifier does not have any child node properties, so we continue.

Then params, because it's an array node, we visit each of them, which is a single node of type Identifier, and we continue.

Now that we're in body, this is a BlockStatement and there's also a body node, and it's also an array node, and we'll continue to visit each of them.

The only property here is the ReturnStatement node, which has an argument and we found BinaryExpression when we visited argument.

BinaryExpression has an operator, a left, and a right.Operator is not a node, it is just a value, so we don't have to continue traversing inwards, we just need to access left and right.

Babel's conversion steps are all this traversal.

Visitors

When we talk about "entering" a node, we are actually visiting them, and we use this term because of the concept of a visitor mode.

Visitor is a cross-language mode for AST traversal.Simply put, they are objects that define the method used to get specific nodes in a tree structure.That's abstract, so let's take an example.

const MyVisitor = {
  Identifier() {
    console.log("Called!");
  }
};

// You can also create a Visitor object and add a method to it later.
let visitor = {};
visitor.MemberExpression = function() {};
visitor.FunctionDeclaration = function() {}

Note: Identifier() {...} is a short form of Identifier: {enter () {...}}..

This is a simple Visitor that calls the Identifier() method whenever an Identifier is encountered in the tree during traversal.

So in the code below, the Identifier() method is called four times (including square, with a total of four Identifiers).).

function square(n) {
  return n * n;
}
path.traverse(MyVisitor);
Called!
Called!
Called!
Called!

These calls occur when you enter a node, but sometimes we can also call Visitor's method on exit.

Suppose we have a tree structure:

- FunctionDeclaration
  - Identifier (id)
  - Identifier (params[0])
  - BlockStatement (body)
    - ReturnStatement (body)
      - BinaryExpression (argument)
        - Identifier (left)
        - Identifier (right)

When we go down through each branch of the tree we end up, so we need to go up and get back to the next node.Walk down the tree We enter each node and exit each node when we walk back up.

Let's walk through this process using the tree above as an example.

  • Enter FunctionDeclaration

    • Enter Identifier (id)
    • To the end
    • Exit Identifier (id)
    • Enter Identifier (params[0])
    • To the end
    • Exit Identifier (params[0])
    • Enter BlockStatement (body)
    • Enter ReturnStatement (body)

      • Enter BinaryExpression (argument)
      • Enter Identifier (left)

        • To the end
      • Exit Identifier (left)
      • Enter Identifier (right)

        • To the end
      • Exit Identifier (right)
      • Exit BinaryExpression (argument)
    • Exit ReturnStatement (body)
    • Exit BlockStatement (body)
  • Exit FunctionDeclaration

So when you create Visitor, you actually have two chances to access a node.

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

If necessary, you can also use | to split the method name into strings in the form of Idenfifier |MemberExpression and apply the same function to multiple access nodes.

Examples in the flow-comments plug-in are as follows:

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

You can also use aliases (such as @babel/types definitions) in Visitor.

For example,

Function is an alias for FunctionDeclaration, FunctionExpression, ArrowFunctionExpression, ObjectMethod and ClassMethod.

const MyVisitor = {
  Function(path) {}
};

Paths (Path)

AST usually has many nodes, so how are the nodes related to each other?We can use a huge variable object that can be manipulated and accessed to represent the association between nodes, or we can use Paths to simplify this..

Path is an object that represents a connection between two nodes.

For example, if there is such a node and its children as:

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

Representing the child node Identifier as a path (Path) looks like this:

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

It also contains other metadata about the path:

{
  "parent": {...},
  "node": {...},
  "hub": {...},
  "contexts": [],
  "data": {},
  "shouldSkip": false,
  "shouldStop": false,
  "removed": false,
  "state": null,
  "opts": null,
  "skipKeys": null,
  "parentPath": null,
  "context": null,
  "container": null,
  "listKey": null,
  "inList": false,
  "parentKey": null,
  "key": null,
  "scope": null,
  "type": null,
  "typeAnnotation": null
}

Of course, the path object also contains many other methods related to adding, updating, moving, and deleting nodes, which we will look at later.

In a sense, path is the location of a node in the tree and a responsive Reactive representation of the various information about that node.When you call a method to modify the tree, the path information is also updated.Babel helps you manage all this so that the nodes can operate as easily as possible and be stateless.

Paths in Visitors

When you use the Identifier() member method in Visitor, you are actually accessing paths, not nodes.In this way, you operate on the responsive representation of the node (that is, the path) rather than on the node itself.

const MyVisitor = {
  Identifier(path) {
    console.log("Visiting: " + path.node.name);
  }
};
a + b + c;
path.traverse(MyVisitor);
Visiting: a
Visiting: b
Visiting: c

State

Consider the following code:

function square(n) {
  return n * n;
}

Let's write a Visitor that renames n to x.

let paramName;

const MyVisitor = {
  FunctionDeclaration(path) {
    const param = path.node.params[0];
    paramName = param.name;
    param.name = "x";
  },

  Identifier(path) {
    if (path.node.name === paramName) {
      path.node.name = "x";
    }
  }
};

For the example code above, this Visitor code may work, but it can easily be broken:

function square(n) {
  return n * n;
}
n;

A better way to do this is to use recursion. Let's put one Visitor inside another.

const updateParamNameVisitor = {
  Identifier(path) {
    if (path.node.name === this.paramName) {
      path.node.name = "x";
    }
  }
};

const MyVisitor = {
  FunctionDeclaration(path) {
    const param = path.node.params[0];
    const paramName = param.name;
    param.name = "x";

    path.traverse(updateParamNameVisitor, { paramName });
  }
};

path.traverse(MyVisitor);

Of course, this is just a deliberately written example, but it demonstrates how to eliminate the global state from Visitor.

Scopes (Scope)

Next let's introduce the concept of scope.

// global scope

function scopeOne() {
  // scope 1

  function scopeTwo() {
    // scope 2
  }
}

In JavaScript, whenever you create a reference, whether through variable s, function s, class es, parameters, module import s, label s, and so on, it belongs to the current scope.

var global = "I am in the global scope";

function scopeOne() {
  var one = "I am in the scope created by `scopeOne()`";

  function scopeTwo() {
    var two = "I am in the scope created by `scopeTwo()`";
  }
}

Deeper internal scope code can use references from outer scopes.

function scopeOne() {
  var one = "I am in the scope created by `scopeOne()`";

  function scopeTwo() {
    one = "I am updating the reference in `scopeOne` inside `scopeTwo`";
  }
}

Inner scopes can also create references with the same name as outer scopes.

function scopeOne() {
  var one = "I am in the scope created by `scopeOne()`";

  function scopeTwo() {
    var one = "I am creating a new `one` but leaving reference in `scopeOne()` alone.";
  }
}

When writing a transformation, care must be taken with scope.We need to make sure that we don't break existing code by changing parts of it.

When adding a new reference, we need to make sure that the name of the new reference does not conflict with any existing references.Or we just want to find all references that use a variable within a given scope.

Scopes can be expressed as follows:

{
  path: path,
  block: path.node,
  parentBlock: path.parent,
  parent: parentScope,
  bindings: [...]
}

WIP: What follows this section

When you create a new scope, you need to give its path and parent scope, and then it will collect all references ("bindings") within that scope during traversal.

Once the references have been collected, you can use a variety of methods on the Scopes, which we will learn about later.

Bindings

All references belong to a specific scope, and this relationship between references and scopes is called binding..

function scopeOnce() {
  var ref = "This is a binding";

  ref; // This is a reference to a binding

  function scopeTwo() {
    ref; // This is a reference to a binding from a lower scope
  }
}

A single binding looks like this:

Text for Translation
{
  identifier: node,
  scope: scope,
  path: path,
  kind: 'var',

  referenced: true,
  references: 3,
  referencePaths: [path, path, path],

  constant: false,
  constantViolations: [path]
}

With this information, you can find all references to a binding and know what type of binding it is (parameters, definitions, and so on), find the scope to which it belongs, or copy its identifier.You even know if it's a constant, if not, which path modified it.

In many cases, it is useful to know if a binding is a constant, and the most useful case is when code is compressed.

function scopeOne() {
  var ref1 = "This is a constant binding";

  becauseNothingEverChangesTheValueOf(ref1);

  function scopeTwo() {
    var ref2 = "This is *not* a constant binding";
    ref2 = "Because this changes the value";
  }
}

API

Babel is actually a collection of modules.In this section, we will explore some of the main modules, explaining what they do and how to use them.

@babel/parser

@babel/parser is the parser for Babel.

First, let's install it.

$ npm install --save @babel/parser

Start by parsing a code string:

// api-test.js
const parser = require("@babel/parser")

const code = `function square(n) {
  return n * n;
}`;

console.log(parser.parse(code))

Function:

// node "babel-demo/src/api-test.js"
Node {
  type: 'File',
  start: 0,
  end: 38,
  loc: SourceLocation {
    start: Position { line: 1, column: 0 },
    end: Position { line: 3, column: 1 }
  },
  errors: [],
  program: Node {
    type: 'Program',
    start: 0,
    end: 38,
    loc: SourceLocation { start: [Position], end: [Position] },
    sourceType: 'script',
    interpreter: null,
    body: [ [Node] ],
    directives: []
  },
  comments: []
}

We can also pass options to the parse() method as follows:

parser.parse(code, {
  plugins: ["jsx"] // default: []
});

@babel/traverse

The @babel/traverse module maintains the state of the entire tree and is responsible for replacing, removing, and adding nodes.

Run the following command to install:

npm install --save @babel/traverse

We can use it with Babylon to traverse and update nodes:

const traverse = require("@babel/traverse").default
const parser = require("@babel/parser")

const code = `function square(n) {
  return n * n;
}`;

const ast = parser.parse(code);

traverse(ast, {
  enter(path) {
    if (
      path.node.type === "Identifier" &&
      path.node.name === "n"
    ) {
      path.node.name = "x";
    }
  }
});

You can run node inspect "babel-demo/src/api-test.js"View modified ast

@babel/types

The @babel/types module is a tool library for AST nodes and contains methods for constructing, validating, and transforming AST nodes, which are useful for writing logic that handles AST.

npm install --save @babel/types

Then use it as follows:

const traverse = require("@babel/traverse").default
const parser = require("@babel/parser")
const t = require("@babel/types")


const code = `function square(n) {
  return n * n;
}`;

const ast = parser.parse(code);

traverse(ast, {
  enter(path) {
    if (t.isIdentifier(path.node, { name: "n" })) {
      path.node.name = "x";
    }
  }
});

Definitions

The @babel/types module has a definition of each single type of node, including information about which attributes the node contains, what legal values are, how to construct the node, traverse the node, and the alias of the node.

A single node type is defined as follows:

defineType("BinaryExpression", {
  builder: ["operator", "left", "right"],
  fields: {
    operator: {
      validate: assertValueType("string")
    },
    left: {
      validate: assertNodeType("Expression")
    },
    right: {
      validate: assertNodeType("Expression")
    }
  },
  visitor: ["left", "right"],
  aliases: ["Binary", "Expression"]
});

Builders

WIP: Write later

You will notice that the BinaryExpression definition above has a builder field..

builder: ["operator", "left", "right"]

This is because each node type has a constructor method builder, which is used like this:

t.binaryExpression("*", t.identifier("a"), t.identifier("b"));

You can create an AST as shown below:

{
  type: "BinaryExpression",
  operator: "*",
  left: {
    type: "Identifier",
    name: "a"
  },
  right: {
    type: "Identifier",
    name: "b"
  }
}

When printed, it looks like this:

a * b

The constructor also validates the nodes it creates and throws a descriptive error in case of misuse, which leads to the next method type.

Validators

WIP: Write later

The definition of BinaryExpression also includes the field fields information of the node and how to validate those fields.

fields: {
  operator: {
    validate: assertValueType("string")
  },
  left: {
    validate: assertNodeType("Expression")
  },
  right: {
    validate: assertNodeType("Expression")
  }
}

Two validation methods can be created.The first is isX..

t.isBinaryExpression(maybeBinaryExpressionNode);

This test ensures that the node is a binary expression, and you can pass in a second parameter to ensure that the node contains specific attributes and values.

t.isBinaryExpression(maybeBinaryExpressionNode, { operator: "*" });

There is also an assertive version of these methods that throws exceptions instead of returning true or false..

t.assertBinaryExpression(maybeBinaryExpressionNode);
t.assertBinaryExpression(maybeBinaryExpressionNode, { operator: "*" });
// Error: Expected type "BinaryExpression" with option { "operator": "*" }

Converters

WIP: Write later

@babel/generator

The @babel/generator module is Babel's code generator, which reads AST and converts it to code and source mappings (sourcemaps).

Run the following command to install it:

npm install --save @babel/generator

Then use it as follows:

const traverse = require("@babel/traverse").default
const parser = require("@babel/parser")
const t = require("@babel/types")
const generate = require("@babel/generator").default;

const code = `function square(n) {
  return n * n;
}`;

const ast = parser.parse(code);

traverse(ast, {
  enter(path) {
    if (t.isIdentifier(path.node, { name: "n" })) {
      path.node.name = "x";
    }
  }
});
console.log(generate(ast, {}, code))

Function

// node "babel-demo/src/api-test.js"
{
  code: 'function square(x) {\n  return x * x;\n}',
  map: null,
  rawMappings: null
}

You can also pass options to the generate() method..

generate(ast, {
  retainLines: false,
  compact: "auto",
  concise: false,
  quotes: "double",
  // ...
}, code);

@babel/template

@babel/template is another small but useful module.It allows you to write string-based, placeholder-based code instead of manual encoding, especially when generating large-scale AST s.In computer science, this capability is called quasiquotes.

$ npm install --save @babel/template
const traverse = require("@babel/traverse").default
// const parser = require("@babel/parser")
const t = require("@babel/types")
const generate = require("@babel/generator").default;

const template = require("@babel/template").default

const buildRequire = template(`
  var IMPORT_NAME = require(SOURCE);
`);

const ast = buildRequire({
  IMPORT_NAME: t.identifier("myModule"),
  SOURCE: t.stringLiteral("my-module")
});

console.log(generate(ast).code);
// node "babel-demo/src/api-test.js"
var myModule = require("my-module");

Write your first Babel plugin

Now that we are familiar with all the basics of Babel, let's write a Babel plug-in.

Start with a function that receives the current babel object as a parameter.

export default function(babel) {
  // plugin contents
}

Remove it directly because it is used frequentlyBabel.typesIt will be more convenient.

export default function({ types: t }) {
// Above equivalent to export default function(babel) {
// let t = babel.types
// This is the object deconstruction in ES2015 grammar
  
  // plugin contents
}

Next, an object whose visitor property is the main Visitor for this plug-in is returned.

export default function({ types: t }) {
  return {
    visitor: {
      // visitor contents
    }
  };
};

Each function in Visitor receives two parameters: path and state

export default function({ types: t }) {
  return {
    visitor: {
      Identifier(path, state) {},
      ASTNodeTypeHere(path, state) {}
    }
  };
};

Let's quickly write a useful plug-in to show how it works.Here is our source code:

foo === bar;

Its AST form is as follows:

{
  type: "BinaryExpression",
  operator: "===",
  left: {
    type: "Identifier",
    name: "foo"
  },
  right: {
    type: "Identifier",
    name: "bar"
  }
}

Let's start by adding the BinaryExpression Visitor method:

export default function({ types: t }) {
  return {
    visitor: {
      BinaryExpression(path) {
        // ...
      }
    }
  };
}

Then let's be more precise, focusing only on those BinaryExpression that use ==.

visitor: {
  BinaryExpression(path) {
    if (path.node.operator !== "===") {
      return;
    }

    // ...
  }
}

Now let's replace the left attribute with a new identifier:

BinaryExpression(path) {
  if (path.node.operator !== "===") {
    return;
  }

  path.node.left = t.identifier("sebmck");
  // ...
}

So if we run this plugin we get:

sebmck === bar;

Now you just need to replace the right attribute.

BinaryExpression(path) {
  if (path.node.operator !== "===") {
    return;
  }

  path.node.left = t.identifier("sebmck");
  path.node.right = t.identifier("dork");
}

That's our final result:

sebmck === dork;

Perfect!Our first Babel plugin.

The complete code is as follows:

// src/api-test.js
function myPlugin({ types: t }) {
  return {
    visitor: {
      BinaryExpression(path) {
        if (path.node.operator !== "===") {
          return;
        }
        path.node.left = t.identifier("sebmck");
        path.node.right = t.identifier("dork");
      }
    }
  };
}

module.exports = myPlugin
// babel.config.js
const path = require("path")
const config = {
  "presets": [
    [
      "@babel/preset-env",
      {
        "useBuiltIns": "entry",
        "targets": {
          "chrome": "72"
        },
        "corejs": 3
      }
    ]
  ],
  "plugins": [path.resolve(__dirname, "./src/api-test")]
}

module.exports = config

Source code:

// src/index.js
foo === bar;

Run NPX Babel src-d build, output:

// build/index.js
sebmck === dork;

Conversion operation

Visit

Get Path of Child Node

In order to get the attribute value of an AST node, we usually access the node first, then usePath.node.propertyMethod is sufficient.

// the BinaryExpression AST node has properties: `left`, `right`, `operator`
BinaryExpression(path) {
  path.node.left;
  path.node.right;
  path.node.operator;
}

If you want to access a path corresponding to an attribute, use the get method of the path object to pass the string form of the attribute as a parameter.

BinaryExpression(path) {
  path.get('left');
}
Program(path) {
  path.get('body.0');
}

Explain in detail as follows:

Will src/api-Test.jsModify to

// src/api-test.js
function myPlugin({ types: t }) {
  return {
    visitor: {
      BinaryExpression(path) {
        debugger
        path.get('left');
      },
      Program(path) {
        debugger
        path.get('body.0');
      }
    }
  };
}
module.exports = myPlugin

Run babel: npx-n inspect Babel src/Index.js-o build

path.get('left') Gets the path corresponding to the left attribute in the node property of the path.

You can seePath.get('body.0') gets the first value of the body array in the path's node property.

Check the type of node

If you want to check the type of node, the best way is:

BinaryExpression(path) {
  if (t.isIdentifier(path.node.left)) {
    // ...
  }
}

You can also shallowly check the properties of a node:

BinaryExpression(path) {
  if (t.isIdentifier(path.node.left, { name: "n" })) {
    // ...
  }
}

Functionally equivalent to:

BinaryExpression(path) {
  if (
    path.node.left != null &&
    path.node.left.type === "Identifier" &&
    path.node.left.name === "n"
  ) {
    // ...
  }
}

Check Path Type

Using path checking can be equivalent to using node checking:

BinaryExpression(path) {
  if (path.get('left').isIdentifier({ name: "n" })) {
    // ...
  }
}

Is equivalent to:

BinaryExpression(path) {
  if (t.isIdentifier(path.node.left, { name: "n" })) {
    // ...
  }
}

Check if the Identifier is referenced

Identifier(path) {
  if (path.isReferencedIdentifier()) {
    // ...
  }
}

Or:

Identifier(path) {
  if (t.isReferenced(path.node, path.parent)) {
    // ...
  }
}

Find a specific parent path

Sometimes you need to traverse the grammar tree from one path up until the appropriate conditions are met.

Callback is called for each parent path and its NodePath is taken as an argument, and its NodePath is returned when the callback returns the true value..

path.findParent((path) => path.isObjectExpression());

You can also determine the current node:

path.find((path) => path.isObjectExpression());

Find the closest parent function or program:

path.getFunctionParent();

Walk up the grammar tree until you find the nearest Statement-type parent node

path.getStatementParent();

Get peer path

If a path is in a list in a Function/Program, it has sibling nodes.

  • UsePath.inListTo determine if a path has siblings,
  • UsePath.getSibling(index) to get peer paths,
  • UsePath.keyGets the index of the container where the path is located,
  • UsePath.containerGet the container of the path (an array containing all sibling nodes)
  • UsePath.listKeyGet the key of the container

These API s are used by the transform-merge-sibling-variables plug-in in @babel/minify

var a = 1; // pathA, path.key = 0
var b = 2; // pathB, path.key = 1
var c = 3; // pathC, path.key = 2
export default function({ types: t }) {
  return {
    visitor: {
      VariableDeclaration(path) {
        // if the current path is pathA
        path.inList // true
        path.listKey // "body"
        path.key // 0
        path.getSibling(0) // pathA
        path.getSibling(path.key + 1) // pathB
        path.container // [pathA, pathB, pathC]
      }
    }
  };
}

Stop traversal

If your plug-in does not need to run under certain circumstances, it is best to return it as soon as possible.

BinaryExpression(path) {
  if (path.node.operator !== '**') return;
}

WIP: Write later

If you subtraverse in the top-level path, you can use two API methods provided:

path.skip() skips traversing the children of the current path. path.stop() stops traversal entirely.

outerPath.traverse({
  Function(innerPath) {
    innerPath.skip(); // if checking the children is irrelevant
  },
  ReferencedIdentifier(innerPath, state) {
    state.iife = true;
    innerPath.stop(); // if you want to save some state and then stop traversal, or deopt
  }
});

Handle

Replace a Node

BinaryExpression(path) {
  path.replaceWith(
    t.binaryExpression("**", path.node.left, t.numberLiteral(2))
  );
}
  function square(n) {
-   return n * n;
+   return n ** 2;
  }

Replace single node with multiple nodes

ReturnStatement(path) {
  path.replaceWithMultiple([
    t.expressionStatement(t.stringLiteral("Is this the real life?")),
    t.expressionStatement(t.stringLiteral("Is this just fantasy?")),
    t.expressionStatement(t.stringLiteral("(Enjoy singing the rest of the song in your head)")),
  ]);
}
  function square(n) {
-   return n * n;
+   "Is this the real life?";
+   "Is this just fantasy?";
+   "(Enjoy singing the rest of the song in your head)";
  }

Replace Node with String Source

FunctionDeclaration(path) {
path.replaceWithSourceString(`function add(a, b) {
    return a + b;
}`);
}
- function square(n) {
-   return n * n;
+ function add(a, b) {
+   return a + b;
  }

Insert Brother Node

FunctionDeclaration(path) {
path.insertBefore(t.expressionStatement(t.stringLiteral("Because I'm easy come, easy go.")));
path.insertAfter(t.expressionStatement(t.stringLiteral("A little high, little low.")));
}
+ "Because I'm easy come, easy go.";
  function square(n) {
    return n * n;
  }
+ "A little high, little low.";

Insert into container

WIP: Not commonly used, only understood.

ClassMethod(path) {
  path.get('body').unshiftContainer('body', t.expressionStatement(t.stringLiteral('before')));
  path.get('body').pushContainer('body', t.expressionStatement(t.stringLiteral('after')));
}
 class A {
  constructor() {
+   "before"
    var a = 'middle';
+   "after"
  }
 }

Delete a Node

FunctionDeclaration(path) {
  path.remove();
}
- function square(n) {
-   return n * n;
- }

Replace parent node

JustPath.parentPath.replaceWithThis replaces the parent node.

BinaryExpression(path) {
  path.parentPath.replaceWith(
    t.expressionStatement(t.stringLiteral("Anyway the wind blows, doesn't really matter to me, to me."))
  );
}
  function square(n) {
-   return n * n;
+   "Anyway the wind blows, doesn't really matter to me, to me.";
  }

Delete parent node

BinaryExpression(path) {
  path.parentPath.remove();
}
  function square(n) {
-   return n * n;
  }

Scope

Check whether local variables are bound

Dig first, fill later

Create a UID

Dig first, fill later

Promote variable declaration to parent scope

Dig first, fill later

Rename bindings and their references

Dig first, fill later

Concluding remarks

Having the knowledge detailed in this article is enough for more than 90% of scenarios where babel should be used.

The next article will cover plug-in options, building nodes, best practices, and more.

There is no more babel in the original paradise.

Tags: Javascript npm Attribute React

Posted on Sat, 13 Jun 2020 12:16:08 -0400 by rtconner