Идея написать такую ​​статью пришла мне в голову, когда я работал над своим транспайлером Webflow / React. Все, что я хотел сделать, это взять строку кода JS и преобразовать ее таким образом, чтобы глобальные переменные не переопределялись, если это уже произошло:

Вначале я думал, что смогу сделать это с некоторой помощью регулярного выражения; но мальчик был я неправ.

Регулярного выражения просто недостаточно, потому что оно полностью игнорирует концепцию переменных с ограниченной областью видимости и работает со строкой, как если бы это был простой текст. Чтобы определить глобальную переменную, мы должны спросить себя: эта переменная уже объявлена ​​в текущей области или в одной из ее родительских областей?

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

Пример AST может выглядеть так:

Пример из статьи Лачезара Николова о JS AST.

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

Вавилон спешит на помощь

Babel - это проект, который изначально начал преобразовывать последний синтаксис es20XX в синтаксис es5 для лучшей совместимости с браузерами. Поскольку комитет Ecmascript постоянно обновляет стандарты языка Ecmascript, плагины предоставляют отличное и удобное решение для простого обновления поведения компилятора Babel.

Babel состоит из множества компонентов, которые работают вместе, чтобы воплотить в жизнь новейший синтаксис Ecmascript. В частности, поток преобразования кода работает со следующими компонентами и следующими отношениями:

  • Синтаксический анализатор анализирует строку кода в структуру представления данных, называемую AST (абстрактное синтаксическое дерево), используя @babel/parser.
  • AST управляется предопределенными плагинами, которые используют@babel/traverse.
  • AST преобразуется обратно в код с использованием @babel/generator.

Теперь вы лучше понимаете Babel и действительно можете понять, что происходит, когда вы создаете подключаемый модуль; и говоря о каком, как мы это делаем?

Сборка и использование плагина Babel

Прежде всего, я хотел бы, чтобы мы поняли AST, сгенерированный Babel, поскольку он необходим для создания подключаемого модуля, поскольку подключаемый модуль будет манипулировать AST, и поэтому нам необходимо это понять. Если вы зайдете на astexplorer.net, вы найдете замечательный компилятор, который преобразует код в AST. Возьмем для примера код foo = "foo". Сгенерированный AST должен выглядеть так:

Как видите, каждый узел в дереве представляет собой часть кода, и он рекурсивен. В выражении присваивания foo = "foo" используется оператор =, операнд слева - это идентификатор с именем foo, а операнд справа - это литерал со значением "foo". Таким образом, каждая часть кода может быть представлена ​​как узел, состоящий из других узлов, каждый узел имеет тип и дополнительные свойства в зависимости от его типа.

Теперь предположим, что мы хотели бы изменить значение "foo" на "bar", гипотетически говоря, что нам нужно сделать, это захватить соответствующий буквальный узел и изменить его значение с "foo" на "bar". Давайте возьмем этот простой пример и превратим его в плагин.

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

  • in.js - включает входной код, который мы хотим преобразовать.
  • out.js - включает вывод только что преобразованного кода.
  • transform.js - принимает код в in.js, преобразует его и записывает новый код в out.js.
  • plugin.js - плагин преобразования, который будет применяться во время преобразования.

Чтобы реализовать наш плагин, скопируйте следующий контент и вставьте его в in.js файл:

foo = "foo"

и следующий контент в файл transform.js:

Чтобы начать преобразование, просто запустите $ node transform.js. Теперь откройте файл out.js, и вы должны увидеть следующее содержимое:

foo = "bar"

Свойство visitor - это место, где должны выполняться фактические манипуляции с AST. Он проходит по дереву и запускает обработчики для каждого указанного типа узла. В нашем случае всякий раз, когда посетитель сталкивается с узлом типа AssignmentExpression node, он заменяет правый операнд на "bar" в случае, если мы присвоим значение "foo" foo. Мы можем добавить обработчик манипуляций для любого типа узла, который нам нужен, это может быть AssignmentExpression, Identifier, Literal или даже Program, который является корневым узлом AST.

Итак, возвращаясь к основной цели, ради которой мы собрались, я сначала напомню вам:

Сначала мы возьмем все глобальные присвоения и превратим их в выражения присваивания членов window, чтобы избежать путаницы и возможных недоразумений. Мне нравится начинать с изучения желаемого вывода AST:

А потом, соответственно, напишем сам плагин:

Теперь я познакомлю вас с двумя новыми концепциями, о которых я раньше не упоминал, но которые используются в плагине выше:

  • Объект types - это служебная библиотека в стиле Lodash для узлов AST. Он содержит методы для создания, проверки и преобразования узлов AST. Это полезно для очистки логики AST с помощью хорошо продуманных служебных методов. Все его методы должны быть эквивалентны типам узлов в верблюжьем корпусе. Все типы определены в @babel/types, и более того, я рекомендую вам взглянуть на исходный код при построении подключаемого модуля, чтобы определить желаемые подписи создателей узлов, поскольку большая часть этого не документирована. Более подробную информацию о types можно найти здесь.
  • Как и объект types, объект scope содержит служебные программы, относящиеся к области действия текущего узла. Он может проверять, определена ли переменная или нет, генерировать уникальные идентификаторы переменных или переименовывать переменные. В плагине выше мы использовали метод hasBinding(), чтобы проверить, есть ли в идентификаторе соответствующая объявленная переменная, поднявшись по AST. Более подробную информацию о scope можно найти здесь.

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

window.foo = 'foo'

В этот код:

if (typeof window.foo === 'undefined') window.foo = 'foo'

Если вы исследуете AST этого кода, то увидите, что мы имеем дело с 3 новыми типами узлов:

  • Унарное выражение - typeof window.foo
  • Двоичное выражение - ... === 'undefined'
  • IfStatement - if (...)

Обратите внимание, как каждый узел состоит из узла над ним. Соответственно обновим наш плагин. Мы сохраним старую логику, в которой мы превращаем глобальные переменные в члены window, и, кроме того, мы сделаем это условным с помощью IfStatement:

Итак, в основном то, что мы делаем здесь, - это проверяем, имеем ли мы дело с выражением присваивания window членов, и если да, мы создадим условный оператор и заменим его текущим узлом. Несколько заметок:

  • Не вдаваясь в подробности объяснения, я создал вложенный ExpressionStatement внутри IfStatement просто потому, что это то, что от меня ожидается, согласно AST.
  • Я использовал метод replaceWith, чтобы заменить текущий узел вновь созданным. Подробнее о методах манипуляции, подобных replaceWith, можно прочитать здесь.
  • Обычно обработчик AssignmentExpression следует вызывать снова, потому что технически я создал новый узел этого типа, когда мы вызывали метод replaceWith, но поскольку я не хочу запускать другой обход для вновь созданных узлов, я вызвал метод skip , иначе у меня была бы бесконечная рекурсия. Подробнее о методах посещения типа skip можно прочитать здесь.

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

Напомним, что всякий раз, когда вы по какой-либо причине забываете, как работает плагин, прочтите эту статью. Когда вы работаете над самим плагином, исследуйте желаемый результат AST на astexplorer.net, а для документации по API я рекомендую вам работать с этим замечательным справочником.