Сегодня мы рассмотрим процесс написания своего собственного плагина babel и опубликуем его в npm.

  1. Что такое babel?

  2. Пишем свой плагин

  3. Публикуем плагин в npm

  4. Добавляем плагин в проект

  5. Заключение

Babel - что это?

Исходя из официальной документации, babel - это набор инструментов, который позволяет преобразовывать код ECMAScript 2015+ в обратно совместимую версию JavaScript.
Например, плагин @babel/plugin-transform-react-jsx преобразовывает *.jsx в *.js:

jsx

const profile = (
  <div>
    <img src="avatar.png" className="profile" />
    <h3>{[user.firstName, user.lastName].join(" ")}</h3>
  </div>
);

js

const profile = React.createElement(
  "div",
  null,
  React.createElement("img", { src: "avatar.png", className: "profile" }),
  React.createElement("h3", null, [user.firstName, user.lastName].join(" "))
);

Вся эта магия происходит благодаря Abstract Syntax Tree (AST). Грубо говоря, AST представляет ваш код как дерево нод (объектов) с определенными свойствами.

Вызов greet('User') представлен в AST справа
Вызов greet('User') представлен в AST справа

Основная идея babel - это взять ваш код -> представить его в дереве AST -> произвести трансформацию узлов дерева -> сгенерировать код из обновленного AST.

Пишем свой плагин

Теперь когда мы познакомились с тем, как работает babel мы можем приступить к написанию собственного плагина. В этом примере плагин будет добавлять брать стили из конфига и применять их для выводов console.

Это мы получим в итоге
Это мы получим в итоге

Для начал создадим новый проект и добавим необходимые зависимости:

mkdir babel-plugin-styled-console-output
cd babel-plugin-styled-console-output
touch index.js
yarn add -D @babel/core

В index.js добавим следующий код:

const babel = require("@babel/core");

module.exports = function () {
  const LogVisitor = {
    CallExpression(path, state) {
      const { callee, arguments } = path.node;

      if (callee.object && callee.object.name === "console") {
        const styles = state.opts.types[callee.property.name];

        if (styles) {
          let parsedStylesToString = "";
          let divider;

          Object.keys(styles).forEach((key) => {
            if (key === "divider") {
              divider = styles[key];
            } else {
              parsedStylesToString += `${key}: ${styles[key]}; `;
            }
          });

          const str = arguments
            .map((arg) => {
              if (arg.type === "StringLiteral") {
                return arg.value;
              }
            })
            .join(divider || " ");

          const newArgs = [
            babel.types.stringLiteral(`%c${str}`),
            babel.types.stringLiteral(parsedStylesToString),
          ];
          path.node.arguments = newArgs;
          path.node.callee.property.name = "log";
        }
      }
    },
  };

  return { visitor: LogVisitor };
};

Теперь пройдем подробнее по тому что мы написали выше:

На строке 4 мы создаем новый объект, который возвращаем на строке 42. Тем самым мы создаем visitor, который будет проходить по нашему коду в момент запуска babel.

Внутри объекта (на строке 5) мы вызываем один из типов в ASTCallExpression. Вы можете спросить откуда вообще взялся этот CallExpression? Хороший вопрос!

Есть замечательный инструмент Babel AST explorer. Давайте перейдем туда и напишем console.log('text');

Кликнув на строку 1, справа мы увидим вышеупомянутое дерево AST. Теперь мы видим что наш вызов console.log представлен в дереве как узел CallExpression с полями callee и arguments. Эти поля мы достаем на строке 6 нашего кода с помощью path.node.

Теперь, когда мы немного понимаем что происходит под капотом, вернемся в наш код. На строке 8 мы проверяем что у нас действительно вызывается метод объекта Console.
На строчке 9 мы достаем данные из нашего конфига (с помощью state.opts), который выглядит следующим образом:

{
  "types": {
    "error": { "color": "white", "background": "blue", "divider": "---" },
  }
}


Далее мы превращаем наш конфиг в строку, что бы в дальнейшем использовать ее как второй аргумент при вызове console.log (встроенная возможность функции log)

console.log('%csometext', 'color: red');

На строке 23 мы собираем все аргументы переданные в log в одну строку и разделяем их с помощью divider переданного в конфиг (или ' ' по умолчанию).

Затем на строке 31 мы создаем новый массив с аргументами и заменяем старые на строке 35. На 36 строке мы заменим .warn, .error и тд. на .log (тк стили доступны только для log функции).

На этом мы закончим с написание плагина и приступим к публикации его в npm.

Публикуем плагин в npm

Для публикации мы будем использовать yarn. В консоле вашего проекта напишите:

yarn init и ответьте на вопросы.

Далее, следуя этой инструкции, мы должны войти/зарегестрироваться в npm.
После того, как вы вошли, запустите:
yarn publishи дождитесь окончания публикации.

Поздравляю! Мы опубликовали наш плагин в npm. Теперь настало время добавить его в проект и настроить.

Добавляем плагин в проект

Я взял свой первый попавшийся проект на React и добавил созданный плагин с помощью:

yarn add -D babel-formatted-log

Далее открыл файл .babelrc / babel.config.json и добавил конфигурацию для плагина:

{
  "plugins": [
    // ...
    [
      "module:babel-formatted-log",
      {
        "types": {
          "error": { "color": "white", "background": "blue", "divider": "---" },
          "log": {
            "border": "1px solid #d64021 ",
            "font-weight": "800",
            "padding": "10px"
          }
        }
      }
    ]
  ]
}

Теперь соберем проект и напишем где-нибудь:

  console.error("babel", "test");

  console.log("text1", "text2", "text3");

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

Заключение

Сегодня мы написали свой собственный babel плагин и опубликовали его в npm. Этот плагин не несет какой-либо практической пользы, он создан в рамках ознакомления :)

Весь код и документация по плагину лежат на Github.

Спасибо что прочитали!

Комментарии (0)