Сегодня я поделюсь с вами пошаговым руководством как написать свой babel плагин. Вы сможете использовать эти знания для автоматизации правок, рефакторинга или кодогенерации.  Babel это JavaScript компилятор, который в основном используют для конвертации кода на ECMAScript 2015+ в обратно совместимый с текущими и более старыми браузерами или окружениями. Babel использует систему плагинов для преобразования кода, поэтому кто угодно может написать свой плагин. Перед тем как мы начнем писать плагин, нужно узнать что такое AST(абстрактное синтаксическое дерево). Я не уверен что могу объяснить это лучше, чем уже сделано в отличных материалах из Сети: Подводя итог, AST это древовидная структура вашего кода. В случае с JavaScript, AST JavaScript следует спецификации estree.  AST представляет собой структуру и значения вашего кода. Таким образом AST позволяет компиляторам вроде babel, понимать код и делать в нем конкретные осмысленные преобразования. Теперь, когда вы знаете что такое AST, давайте напишем плагин для преобразования кода используя эти знания. Пример ниже, это общий шаблон использования babel для трансформации кода: Вам нужно установить @babel/core для запуска данного примера. @babel/parser, @babel/traverse, @babel/generator все это зависимости @babel/core поэтому его установки будет достаточно. Основная идея состоит в том чтобы распарсить ваш код в AST, изменить AST и после сгенерировать из него код. Однако, мы можем использовать другое babel API записав пример выше следующим образом: Это наш первый babel плагин который заменяет все переменные с именем n на x, разве не круто? Выделите функцию myCustomPlugin в отдельный файл и экспортируйте её. Опубликуйте npm пакет и сможете гордо сказать что сделали babel плагин! К этому моменту вы должно быть думаете: "Да, мы написали babel плагин, но я все еще не представляю как это работает...". Не волнуйтесь, сейчас мы погрузимся глубже в то как написать плагин самостоятельно. В следующем примере я хочу разыграть своего коллегу созданием плагина который будет: До: После: Мы сохранили console.log, поэтому несмотря на то что код сложно читать, он все еще нормально работает. (Я ведь не хочу сломать код на продакшене!) Воспользуемся babel AST explorer, наведите курсором на разные части кода и наблюдайте как они представлены в AST:  Если это первый раз когда вы видите AST, поэкспериментируйте с ним чуть дольше, чтобы понять как связаны части дерева и код. Итак, теперь мы знаем что нам нужна цель: Вернемся снова в babel AST explorer, но в этот раз с кодом который мы хотим сгенерировать:  Поиграйте с этим кодом и подумайте как мы можем превратить предыдущее AST дерево в текущее.  Например мы видим что 'H' + 'e' + 'l' + 'l' + 'o' + ' ' + eman сформирован вложенными бинарными выражениями(BinaryExpression) со строковыми литералами(StringLiteral) Взглянем на наш код снова: Преобразование кода использует паттерн посетителя так же известный как visitor. Во время обхода дерева, babel с помощью поиска в глубину обойдет каждый узел AST дерева. Мы можем указать колбэк в visitor, таким образом каждый раз когда будет найден новый узел, babel вызовет колбэк с таким узлом. В объекте visitor можно уточнить название какого узла будет срабатывать колбэк: Запустив код выше, вы увидите вывод идентификатор и строковый литерал всякий раз когда babel будет их находить: Прежде чем продолжить, давай посмотрим на параметр Identifer(path) {}. Он называется path вместо node, в чем между ними разница? В babel, path это абстракция над узлом, которая обеспечивает связь между ними, предоставляя доступ к такой информации как родительский узел, области видимости, контексту и т.д. Кроме этого path предоставляет методы, такие как replaceWith, insertBefore, remove и прочие, которые могут повлиять на исходный AST узел. Вы можете узнать больше деталей о path в руководстве по babel от Jamie Kyle  Продолжим писать плагин. Как мы видели в babel AST explorer, название идентификатора хранится в свойстве name, исходя из этого, обратный порядок букв в названиях переменных реализуется так: После запуска мы увидим: Мы почти добились цели исключая тот факт что функция console.log так же подверглась изменениям. Как мы можем это предотвратить? Снова взглянем на AST:  console.log является частью MemberExpression, c object — "console" и property — "log" Поэтому мы будем проверять, если наш текущий идентификатор содержится внутри MemberExpression, то не будем менять порядок букв: Да, теперь это работает правильно! Итак, почему мы проверяем что Identifier родителя не является console.log MemberExpression? Почему бы просто не сделать проверку что Identifier.name === 'console' || Identifier.name === 'log'? Мы можем так сделать, однако тогда переменные названные console or log не будут изменены: Как узнать методы isMemberExpression и isIdentifier? Все типы узлов описанные в @babel/types имеют функцию валидатор типа isXxxx, например функция anyTypeAnnotation будет иметь валидатор isAnyTypeAnnotation. Если вы хотите получить полный список функций валидаторов, можете ознакомиться с исходным кодом Следующим шагом является создание вложенного BinaryExpression из строкового литерала. Чтобы создать узел в AST, можно использовать утилиту из [@babel/types](https://babeljs.io/docs/en/babel-types): Мы разделили содержание StringLiteral, которое находится в path.node.value, cделали каждый символ отдельным StringLiteral и объединили с помощью BinaryExpression. И наконец заменили StringLiteral новым узлом. …И на этом все! За исключением того факта что запустив, мы столкнемся с переполнением стека: RangeError: Maximum call stack size exceeded Почему? Так происходит потому, что на каждый StringLiteral мы создаем StringLiteral, на которые тоже в свою очередь создаем StringLiteral. Несмотря на то что мы заменяем один StringLiteral другим StringLiteral, babel будет считать это за добавление нового узла, в результате чего сработает колбэк, вызвав бесконечную рекурсию и как следствие переполнение стека.  Итак, как можно сказать babel что после того как мы заменили StringLiteral на newNode, он может остановится и больше не искать новые узлы? Мы можем использовать path.skip() для того чтобы пропустить обход дочерних элементов: …Теперь это работает без переполнения стека! Вот такой у нас получился плагин для babel: Краткое описание шагов, которые мы сделали: Если вы хотите узнать больше, репозиторий babel на GitHub лучшее место чтобы найти примеры различных преобразований с помощью babel. Взгляните на https://github.com/babel/babel, и найдите директории babel-plugin-transform-* или babel-plugin-proposal-*, все они все являются плагинами-преобразователями, среди них можно найти как babel трансформирует nullish coalescing operator, optional chaining и многое другое. Фронтенд разработчик