Skip to content

Latest commit

 

History

History
1767 lines (1377 loc) · 54 KB

File metadata and controls

1767 lines (1377 loc) · 54 KB

Руководство по написанию плагинов для Babel

В этом документе описано как создавать плагины для Babel.

Если вы читаете это не на английском языке, то вы можете наткнуться на разделы на английском, которые ещё не переведены. Если вы хотите помочь с переводом, то вы должны делать это, используя Crowdin. Прочтите, пожалуйста, рекомендации для подробной информации.

cc-by-4.0

Отдельное спасибо @sebmck, @hzoo, @jdalton, @abraithwaite, @robey и другим за их невероятную помощь в создании этого руководства.

Node Packaged Manuscript

Вы можете установить это руководство с помощью npm. Просто выполните:

$ npm install -g babel-plugin-handbook

Теперь вам доступна команда babel-plugin-handbook, которая открывает этот readme-файл в вашем $PAGER. Или же вы можете продолжить читать этот документ, как вы делаете это сейчас.

Переводы

Request another translation

Если вы читаете этот документ на на английском языке, вы найдёте некоторое количество английских слов, которые являются программистскими терминами. Если бы они были переведены на другие языки это привело бы к отсутствию последовательности и плавности, когда вы читаете о них. Во многих случаях вы найдете дословные переводы с последующим английским термином в скобках (). Например: Абстрактные Синтаксические Деревья (ASTs).

Содержание

Введение

Babel - это многоцелевой компилятор общего назначения для JavaScript. Более того, это коллекция модулей, которая может быть использована для множества различных форм синтаксического анализа.

Статический анализ - это процесс анализа кода без запуска этого кода. (Анализ кода во время выполнения известен как динамический анализ). Цели синтаксического анализа очень разнообразны. Это может быть использовано для контроля качества кода (linting), компиляции, подсветки синтаксиса, трансформации, оптимизации, минификации и много другого.

Вы можете использовать Babel для создания множества различных инструментов, которые помогут вам стать более продуктивным и писать лучшие программы.

Базовые концепции

Babel - это JavaScript компилятор, точнее компилятор, преобразующий программу на одном языке в программу на другом языке (source-to-source compiler), часто называемый трянслятор. Это означает, что вы даёте Babel некоторый JavaScript код, а Babel модифицирует его, генерирует новый код и возвращает его.

Абстрактные синтаксические деревья (ASTs)

Каждый из этих шагов требует создания или работы с Абстрактным синтаксическим деревом, или AST.

Babel использует в качестве AST модифицированный ESTree и его спецификация находится здесь.

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

Взгляните на AST Explorer чтобы получить более полное представление об AST-нодах. Здесь находится ссылка на него с уже скопированным примером выше.

Эта же программа может быть представлена в виде подобного списка:

- 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

Или в виде JavaScript-объекта, вроде этого:

{
  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"
        }
      }
    }]
  }
}

Вы могли заметить, что каждый уровень AST имеет одинаковую структуру:

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

Некоторые свойства были убраны для упрощения.

Каждый из этих уровней называется Нода (Node). Отдельный AST может состоять как из одной ноды, так и из сотен, если не тысяч нод. Все вместе они способны описать синтаксис программы, который может быть использован для статического анализа.

Каждая нода имеет следующий интерфейс:

interface Node {
  type: string;
}

Поле type - это строка, описывающая, чем является объект, представляемый данной нодой (т.е. "FunctionDeclaration", "Identifier", или "BinaryExpression"). Каждый тип ноды определяет некоторый дополнительный набор полей, описывающий этот конкретный тип.

Пример. Каждая нода, сгенерированная Babel, имеет дополнительные свойства, которые описывают позицию этой ноды в оригинальном исходном коде.

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

Эти свойства - start, end, loc - присутствуют в каждой отдельной ноде.

Этапы работы Babel

Три основных этапа работы Babel это парсинг, трансформация, генерация.

Парсинг

Стадия разбора принимает код и выводит AST. Существуют два этапа разбора в Babel: Лексический анализ и Синтаксический анализ.

Лексический анализ

Лексический анализ будет принимать строку кода и превращать его в поток токенов.

Вы можете думать о токенах как о плоском массиве элементов синтаксиса языка.

n * n;
[
  { type: { ... }, value: "n", start: 0, end: 1, loc: { ... } },
  { type: { ... }, value: "*", start: 2, end: 3, loc: { ... } },
  { type: { ... }, value: "n", start: 4, end: 5, loc: { ... } },
  ...
]

Каждый тип здесь имеет набор свойств, описывающих токен:

{
  type: {
    label: 'name',
    keyword: undefined,
    beforeExpr: false,
    startsExpr: true,
    rightAssociative: false,
    isLoop: false,
    isAssign: false,
    prefix: false,
    postfix: false,
    binop: null,
    updateContext: null
  },
  ...
}

Как узлы AST, они также имеют start, end и loc.

Синтаксический анализ

Синтаксический анализ примет поток токенов и преобразует их в AST представление. Используя информацию в токенах, этот этап переформатирует их как AST, который отображает структуру кода таким образом, что облегчает работу с ним.

Трансформация

Этап преобразования принимает AST и проходит через него, добавляя, обновляя, и удаляя узлы по мере прохождения. Это, безусловно, наиболее сложная часть Babel или любого компилятора. Здесь работают плагины и это будет предметом обсуждения большей части этого руководства. Поэтому мы не погружаемся слишком глубоко прямо сейчас.

Генерация

Этапе генерации кода принимает окончательное AST и преобразует его в сроку кода, так же создавая source maps.

Генерация кода довольно проста: вы проходите через AST в глубину, строя строку, которая представляет преобразованный код.

Обход

Когда вы хотите трансорфмировать AST вам необходимо пройти по всему дереву рекурсивно.

Скажем, у нас есть тип FunctionDeclaration. Он имеет несколько свойств: id, params и body. Каждый из них имеет вложенные узлы.

{
  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"
        }
      }
    }]
  }
}

Итак, мы начинаем с FunctionDeclaration, и мы знаем его внутренние свойства, поэтому мы посещаем каждое из них и их детей в по порядку.

Далее мы идем к id, который представляет собой Identifier. Identifierы не имеют дочерних узлов, поэтому мы двигаемся дальше.

Затем следует params, который представляет собой массив узлов, и мы посещаем каждый из них. В данном случае это один узел, который также является Identifier. Идем дальше.

Затем мы попали тело, которое является BlockStatement со свойством body, которое является массивом узлов, поэтому мы проходимся по каждому из них.

Единственным элементом здесь является узел ReturnStatement, который имеет argument, мы идем на argument и находим выражение BinaryExpression.

BinaryExpression имеет оператор, левую часть и правую часть. Оператор это не узел, а просто значение, поэтому мы не переходим к нему, и вместо этого просто посещаем левую и правую части.

Этот процесс обхода происходит в Babel на этапе преобразования.

Посетители

When we talk about "going" to a node, we actually mean we are visiting them. The reason we use that term is because there is this concept of a visitor.

Visitors are a pattern used in AST traversal across languages. Simply put they are an object with methods defined for accepting particular node types in a tree. That's a bit abstract so let's look at an example.

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

Note: Identifier() { ... } is shorthand for Identifier: { enter() { ... } }.

This is a basic visitor that when used during a traversal will call the Identifier() method for every Identifier in the tree.

So with this code the Identifier() method will be called four times with each Identifier (including square).

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

These calls are all on node enter. However there is also the possibility of calling a visitor method when on exit.

Представьте, что у нас есть следующая древовидная структура:

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

As we traverse down each branch of the tree we eventually hit dead ends where we need to traverse back up the tree to get to the next node. Going down the tree we enter each node, then going back up we exit each node.

Давайте пройдем через этот процесс для дерева из примера выше.

  • Вход в FunctionDeclaration
    • Вход в Identifier (id)
    • Попадание в тупик
    • Выход из Identifier (id)
    • Вход в Identifier (params[0])
    • Попадание в тупик
    • Выход из Identifier (params[0])
    • Вход в BlockStatement (body)
    • Вход в ReturnStatement (body)
      • Вход в BinaryExpression (argument)
      • Вход в Identifier (left)
        • Попадание в тупик
      • Выход из Identifier (left)
      • Вход в Identifier (right)
        • Попадание в тупик
      • Выход из Identifier (right)
      • Выход из BinaryExpression (argument)
    • Выход из ReturnStatement (body)
    • Выход из BlockStatement (body)
  • Выход из FunctionDeclaration

Итак, при создании посетителей у вас есть две возможности для посещения узла.

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

Пути

An AST generally has many Nodes, but how do Nodes relate to one another? We could have one giant mutable object that you manipulate and have full access to, or we can simplify this with Paths.

Путь — это объектное представление ссылки между двумя узлами.

Например, если мы возьмем следующий узел и его дочерний:

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

And represent the child Identifier as a path, it looks something like this:

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

Он также имеет дополнительные метаданные о пути:

{
  "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
}

As well as tons and tons of methods related to adding, updating, moving, and removing nodes, but we'll get into those later.

In a sense, paths are a reactive representation of a node's position in the tree and all sorts of information about the node. Whenever you call a method that modifies the tree, this information is updated. Babel manages all of this for you to make working with nodes easy and as stateless as possible.

Paths in Visitors

When you have a visitor that has a Identifier() method, you're actually visiting the path instead of the node. This way you are mostly working with the reactive representation of a node instead of the node itself.

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

Состояние

State is the enemy of AST transformation. State will bite you over and over again and your assumptions about state will almost always be proven wrong by some syntax that you didn't consider.

Возьмем следующий код:

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

Let's write a quick hacky visitor that will rename 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";
    }
  }
};

This might work for the above code, but we can easily break that by doing this:

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

Лучший способ справиться с этим — рекурсия. Так что давайте делать как в фильме Кристофера Нолан и положить посетителя в посетителя.

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 });
  }
};

Конечно это надуманный пример, но он демонстрирует как исключить глобальное состояние из ваших посетителей.

Области видимости

Далее введем понятие области видимости. JavaScript имеет лексическую область видимости, которая имеет структуру дерева, где блоки создают новую область видимости.

// global scope

function scopeOne() {
  // scope 1

  function scopeTwo() {
    // scope 2
  }
}

Whenever you create a reference in JavaScript, whether that be by a variable, function, class, param, import, label, etc., 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()`";
  }
}

Код в пределах более глубокой области видимости может использовать ссылку из более высокой области видимости.

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

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

Более низкая область видимости также может создать ссылку с тем же именем без её изменения.

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 transform, we want to be wary of scope. We need to make sure we don't break existing code while modifying different parts of it.

We may want to add new references and make sure they don't collide with existing ones. Or maybe we just want to find where a variable is referenced. We want to be able to track these references within a given scope.

Область видимости может быть представлена как:

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

When you create a new scope you do so by giving it a path and a parent scope. Then during the traversal process it collects all the references ("bindings") within that scope.

Once that's done, there's all sorts of methods you can use on scopes. We'll get into those later though.

Bindings

References all belong to a particular scope; this relationship is known as a 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:

{
  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 the references to a binding, see what type of binding it is (parameter, declaration, etc.), lookup what scope it belongs to, or get a copy of its identifier. You can even tell if it's constant and if not, see what paths are causing it to be non-constant.

Being able to tell if a binding is constant is useful for many purposes, the largest of which is minification.

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'll walk through the major ones, explaining what they do and how to use them.

Note: This is not a replacement for detailed API documentation which will be available elsewhere shortly.

Babylon is Babel's parser. Started as a fork of Acorn, it's fast, simple to use, has plugin-based architecture for non-standard features (as well as future standards).

Сперва давайте установим его.

$ npm install --save babylon

Давайте начнем с разбора строки кода:

import * as babylon from "babylon";

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

babylon.parse(code);
// Node {
//   type: "File",
//   start: 0,
//   end: 38,
//   loc: SourceLocation {...},
//   program: Node {...},
//   comments: [],
//   tokens: [...]
// }

Мы также можем передать опции в parse() следующим образом:

babylon.parse(code, {
  sourceType: "module", // default: "script"
  plugins: ["jsx"] // default: []
});

sourceType может быть как "module", так и "script" и представляет собой режим разбора в Babylon. "module" будет производить разбор в strict mode и позволяет объявления модуля, "script" — нет.

Примечание: значением sourceType по умолчанию является "script", и если будет найден import или export, то произойдет ошибка. Передайте sourceType: "module" чтобы избежать этой ошибки.

Поскольку Babylon имеет плагин-архитектуру, есть также опция plugins, которая включит внутренние плагины. Обратите внимание, что в этот API Babylon еще не открыт для внешних плагинов, но может сделать это в будущем.

Чтобы увидеть полный список плагинов, посетите Babylon README.

The Babel Traverse module maintains the overall tree state, and is responsible for replacing, removing, and adding nodes.

Установите его, выполнив:

$ npm install --save babel-traverse

Мы можем использовать его вместе с Babylon для обхода и обновления узлов:

import * as babylon from "babylon";
import traverse from "babel-traverse";

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

const ast = babylon.parse(code);

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

Babel Types is a Lodash-esque utility library for AST nodes. It contains methods for building, validating, and converting AST nodes. It's useful for cleaning up AST logic with well thought out utility methods.

Его можно установить, запустив:

$ npm install --save babel-types

Затем начнете его использовать:

import traverse from "babel-traverse";
import * as t from "babel-types";

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

Определения

Типы Babel имеют определения для каждого типа узла с информацией о том, какие свойства чему принадлежат, какие значения являются допустимыми, как построить этот узел, как узел должен быть пройден, а также псевдонимы узла.

Определение типа узла выглядит следующим образом:

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"]
});

Строители

Обратите внимание, что в приведенном выше определении для BinaryExpression есть поле builder.

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

Это потому, что каждый тип узла получает метод-строитель, который при использовании выглядит следующим образом:

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

И создает следующий AST:

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

Который при выводе выглядит следующим образом:

a * b

Строители также будут проверять узлы, которые они создают и бросить ошибки если используются ненадлежащим образом. Что приводит нас к необходимости познакомиться со следующим типом методов.

Валидаторы

The definition for BinaryExpression also includes information on the fields of a node and how to validate them.

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

This is used to create two types of validating methods. The first of which is isX.

t.isBinaryExpression(maybeBinaryExpressionNode);

This tests to make sure that the node is a binary expression, but you can also pass a second parameter to ensure that the node contains certain properties and values.

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

There is also the more, ehem, assertive version of these methods, which will throw errors instead of returning true or false.

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

Преобразователи

[WIP]

Babel Generator — это генератор кода Babel. Он принимает AST и превращает его в код с sourcemaps.

Выполните следующие действия, чтобы установить его:

$ npm install --save babel-generator

Затем используйте его

import * as babylon from "babylon";
import generate from "babel-generator";

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

const ast = babylon.parse(code);

generate(ast, null, code);
// {
//   code: "...",
//   map: "..."
// }

Вы также можете передать параметры в generate().

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

Babel Template is another tiny but incredibly useful module. It allows you to write strings of code with placeholders that you can use instead of manually building up a massive AST.

$ npm install --save babel-template
import template from "babel-template";
import generate from "babel-generator";
import * as t from "babel-types";

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);
var myModule = require("my-module");

Создание вашего первого плагина Babel

Теперь, когда вы знакомы с основами Babel, давайте свяжем это вместе с API для плагинов.

Начнём с функции, в которую передается текущий babel-объект.

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

Так как вы будете использовать его так часто, вы, скорее всего, захотите просто взять babel.types следующим образом:

export default function({ types: t }) {
  // plugin contents
}

Затем вы возвращаете объект со свойством visitor, который является основным посетитель для плагина.

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

Давайте быстро напишем плагин, чтобы показать как это работает. Вот наш исходный код:

foo === bar;

Или в виде AST:

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

Мы начнем с добавления метода посетителя BinaryExpression.

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

Затем давайте сведем это к BinaryExpression, которые используют оператор ===.

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

    // ...
  }
}

Теперь давайте заменить свойство left новым идентификатором:

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

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

Уже теперь если мы запустим этот плагин, мы получим:

sebmck === bar;

Теперь давайте просто заменим свойство right.

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

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

И теперь для нашего конечного результата:

sebmck === dork;

Великолепно! Наш самый первый плагин для Babel.


Операции преобразования

Посещение

Check if a node is a certain type

Если вы хотите проверить тип узла, то лучше всего сделать это следующим образом:

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

Вы также можете сделать поверхностную проверку свойств в этом узле:

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

Это функционально эквивалентно:

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

Check if an identifier is referenced

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

В качестве альтернативы:

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

Манипуляция

Замена узла

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

Замена узла несколькими узлами

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)";
  }

Примечание: При замене выражения с несколькими узлами, они должны быть выражениями. Это потому, что Babel широко использует эвристику, при замене узлов, что означает, что вы можете сделать некоторые довольно сумасшедшие преобразования, которые в противном случае были бы чрезвычайно многословные.

Замена узла исходной строкой

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

Note: It's not recommended to use this API unless you're dealing with dynamic source strings, otherwise it's more efficient to parse the code outside of the visitor.

Добавление узла-потомка

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.";

Note: This should always be a statement or an array of statements. This uses the same heuristics mentioned in Replacing a node with multiple nodes.

Удаление узла

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

Замена родителя

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.";
  }

Удаление родителя

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

Область видимости

Checking if a local variable is bound

FunctionDeclaration(path) {
  if (path.scope.hasBinding("n")) {
    // ...
  }
}

This will walk up the scope tree and check for that particular binding.

You can also check if a scope has its own binding:

FunctionDeclaration(path) {
  if (path.scope.hasOwnBinding("n")) {
    // ...
  }
}

Создание UID

Следующий код сгенерирует идентификатор, который не конфликтует ни содной из локально определенных переменных.

FunctionDeclaration(path) {
  path.scope.generateUidIdentifier("uid");
  // Node { type: "Identifier", name: "_uid" }
  path.scope.generateUidIdentifier("uid");
  // Node { type: "Identifier", name: "_uid2" }
}

Pushing a variable declaration to a parent scope

Sometimes you may want to push a VariableDeclaration so you can assign to it.

FunctionDeclaration(path) {
  const id = path.scope.generateUidIdentifierBasedOnNode(path.node.id);
  path.remove();
  path.scope.parent.push({ id, init: path.node });
}
- function square(n) {
+ var _square = function square(n) {
    return n * n;
- }
+ };

Rename a binding and its references

FunctionDeclaration(path) {
  path.scope.rename("n", "x");
}
- function square(n) {
-   return n * n;
+ function square(x) {
+   return x * x;
  }

Alternatively, you can rename a binding to a generated unique identifier:

FunctionDeclaration(path) {
  path.scope.rename("n");
}
- function square(n) {
-   return n * n;
+ function square(_n) {
+   return _n * _n;
  }

Параметры плагина

Если вы хотите позволить пользователям настраивать поведение вашего плагина для Babel, вы можете принимать параметры, специфичные для этого плагина, которые пользователи могут указать следующим образом:

{
  plugins: [
    ["my-plugin", {
      "option1": true,
      "option2": false
    }]
  ]
}

Затем эти параметры передаются в посетителей через объект state:

export default function({ types: t }) {
  return {
    visitor: {
      FunctionDeclaration(path, state) {
        console.log(state.opts);
        // { option1: true, option2: false }
      }
    }
  }
}

Эти параметры зависят от плагина, и вам не удается получить доступ к параметрам от других плагинов.


Построение узлов

When writing transformations you'll often want to build up some nodes to insert into the AST. As mentioned previously, you can do this using the builder methods in the babel-types package.

The method name for a builder is simply the name of the node type you want to build except with the first letter lowercased. For example if you wanted to build a MemberExpression you would use t.memberExpression(...).

The arguments of these builders are decided by the node definition. There's some work that's being done to generate easy-to-read documentation on the definitions, but for now they can all be found here.

A node definition looks like the following:

defineType("MemberExpression", {
  builder: ["object", "property", "computed"],
  visitor: ["object", "property"],
  aliases: ["Expression", "LVal"],
  fields: {
    object: {
      validate: assertNodeType("Expression")
    },
    property: {
      validate(node, key, val) {
        let expectedType = node.computed ? "Expression" : "Identifier";
        assertNodeType(expectedType)(node, key, val);
      }
    },
    computed: {
      default: false
    }
  }
});

Here you can see all the information about this particular node type, including how to build it, traverse it, and validate it.

By looking at the builder property, you can see the 3 arguments that will be needed to call the builder method (t.memberExpression).

builder: ["object", "property", "computed"],

Note that sometimes there are more properties that you can customize on the node than the builder array contains. This is to keep the builder from having too many arguments. In these cases you need to set the properties manually. An example of this is ClassMethod.

You can see the validation for the builder arguments with the fields object.

fields: {
  object: {
    validate: assertNodeType("Expression")
  },
  property: {
    validate(node, key, val) {
      let expectedType = node.computed ? "Expression" : "Identifier";
      assertNodeType(expectedType)(node, key, val);
    }
  },
  computed: {
    default: false
  }
}

You can see that object needs to be an Expression, property either needs to be an Expression or an Identifier depending on if the member expression is computed or not and computed is simply a boolean that defaults to false.

So we can construct a MemberExpression by doing the following:

t.memberExpression(
  t.identifier('object'),
  t.identifier('property')
  // `computed` is optional
);

Which will result in:

object.property

However, we said that object needed to be an Expression so why is Identifier valid?

Well if we look at the definition of Identifier we can see that it has an aliases property which states that it is also an expression.

aliases: ["Expression", "LVal"],

So since MemberExpression is a type of Expression, we could set it as the object of another MemberExpression:

t.memberExpression(
  t.memberExpression(
    t.identifier('member'),
    t.identifier('expression')
  ),
  t.identifier('property')
)

Which will result in:

member.expression.property

It's very unlikely that you will ever memorize the builder method signatures for every node type. So you should take some time and understand how they are generated from the node definitions.

You can find all of the actual definitions here and you can see them documented here


Лучшие практики

I'll be working on this section over the coming weeks.

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

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

path.traverse({
  Identifier(path) {
    // ...
  }
});

path.traverse({
  BinaryExpression(path) {
    // ...
  }
});

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.

path.traverse({
  Identifier(path) {
    // ...
  },
  BinaryExpression(path) {
    // ...
  }
});

Do not traverse when manual lookup will do

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

const visitorOne = {
  Identifier(path) {
    // ...
  }
};

const MyVisitor = {
  FunctionDeclaration(path) {
    path.get('params').traverse(visitorOne);
  }
};

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.

const MyVisitor = {
  FunctionDeclaration(path) {
    path.node.params.forEach(function() {
      // ...
    });
  }
};

Optimizing nested visitors

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

const MyVisitor = {
  FunctionDeclaration(path) {
    path.traverse({
      Identifier(path) {
        // ...
      }
    });
  }
};

However, this creates a new visitor object everytime FunctionDeclaration() is called above, which Babel then needs to explode and validate every single time. This can be costly, so it is better to hoist the visitor up.

const visitorOne = {
  Identifier(path) {
    // ...
  }
};

const MyVisitor = {
  FunctionDeclaration(path) {
    path.traverse(visitorOne);
  }
};

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

const MyVisitor = {
  FunctionDeclaration(path) {
    var exampleState = path.node.params[0].name;

    path.traverse({
      Identifier(path) {
        if (path.node.name === exampleState) {
          // ...
        }
      }
    });
  }
};

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

const visitorOne = {
  Identifier(path) {
    if (path.node.name === this.exampleState) {
      // ...
    }
  }
};

const MyVisitor = {
  FunctionDeclaration(path) {
    var exampleState = path.node.params[0].name;
    path.traverse(visitorOne, { exampleState });
  }
};

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.

class Foo {
  constructor() {
    // ...
  }
}
const constructorVisitor = {
  ClassMethod(path) {
    if (path.node.name === 'constructor') {
      // ...
    }
  }
}

const MyVisitor = {
  ClassDeclaration(path) {
    if (path.node.id.name === 'Foo') {
      path.traverse(constructorVisitor);
    }
  }
}

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

class Foo {
  constructor() {
    class Bar {
      constructor() {
        // ...
      }
    }
  }
}