Доброго времени суток, меня зовут Владимир Миленко, я Frontend-разработчик в компании Lightspeed, и сегодня мы поговорим о проблеме отсутствия компонентов в том или ином фреймворке и попытках автоматически конвертировать их.  Исторически сложилось, что и в eCommerce, и в Retail продуктах для админ-панелей мы используем React.JS в качестве основного фреймворка, однако платформа для ресторанов использует Angular, что не позволяет им использовать нашу библиотеку компонентов. Перед моим отпуском эта проблема стала острее, ввиду необходимости приведения UI/UX к одному виду. Мною было принято решение провести небольшое исследование на тему миграции компонентов, сделать Proof of Concept и поделиться ощущениями. Об этом и будет данный пост.  Для понимания дальнейшего необходимо знать следующие обозначения:  AST — абстрактное синтаксическое дерево, это представление кода в виде дерева, в нем отсутствуют скобки и т.д. Пример частого использования AST — babel, он строит AST с помощью парсеров, а затем происходит транспиляция листьев с типами, введенными в es6 — в es5-поддерживаемые.  https://ru.m.wikipedia.org/wiki/Абстрактное_синтаксическое_дерево  Первое, что пришло в голову — конечно же напрямую конвертировать из React в Angular, затем подумав хорошо (а в отпуске такое не всегда получается), эта идея была полностью отвергнута ввиду отсутствия возможностей прямой конвертации без промежуточного дерева.  Хорошая мысль — универсальное дерево, обладающее большей абстракцией, более верхнеуровневое, если можно так сказать. Основная мысль заключалась в том, чтобы сложные конструкции переводить в более абстрактные, о примерах чуть позже.  Процесс будет выглядеть примерно так:  Основные принципы, на которых устроен мир(зачеркнуть), это парсер. Я пришел к мысли, что самое лучшее — построить все это дело на матчерах и предикатах.  Для теоретической работы парсера нам необходимо описать функцию-матчер и функцию-парсер, но мы добавим еще и желаемый тип входной ноды, чтобы ограничить список матчеров, в которых мы будем проверять текущую ноду(не оптимизации ради, а удобства для). Матчер возвращает true/false в зависимости от ноды, переданной в неё. Он делает проверки на ноде, тот ли это парсер, который необходим, или нет.  Если матчер вернул true, мы будем вызывать уже функцию парсинга. Функция-парсер должна вернуть UST ноду — абстрактное описание того, что происходило в разбираемой AST-ноде.  Основная концепция парсинга – каждый парсер должен иметь возможность вызвать парсинг нод, не влияя не результирующее дерево. Это нам пригодится как в матчинге, так и в парсинге, поскольку мы будем генерировать и child-ов, и иногда отталкиваться от child-ов.  Пожалуй, это одна из самых интересных задач. Как мы все знаем, React-компонент может брать параметры из множества мест: state, props, context, outer scope.  На этапе PoC мы рассматриваем только props, и даже только в определенной конструкции, но об этом позже.  Итак, в AST нет как такового трекинга переменной, т.е. при взгляде на определенную ноду вы увидите Identifier, но это не даст вам ни малейшего понятия, откуда именно эта переменная взялась.  На помощь придет AST-traversal, который подскажет текущую область видимости той или иной ноды.  Будем считать входным параметром компонента следующую конструкцию:  const {a} = this.props; Другими словами, будем искать VariableDeclaration, где id — ObjectPattern, а init — MemberExpression, с property — обязательно 'props'.  Используемые инструменты: Интерфейс предиката для парсинга: Ну и сразу пример имплементации: Исходя из кода выше станет понятно, что данный предикат ждет на вход JSXExpressionContainer, далее идут различные проверки, чтобы определить, действительно ли этот парсер нужен для входной ноды.
Данный предикат сработает на следующую JSX-конструкцию: Функция парсинга разбирает конструкцию на кусочки, она так же позволяет найти мутации исходного параметра, Далее идет процесс определения переменной, за это отвечает функция resolveVariable. Она служит для определения области видимости и поиска определения данной переменной: В данном коде мы фиксированно ищем const {varName} = this.props. Поскольку это PoC, этого вполне достаточно. Возвращает эта функция uuid с идентификатором переменной, чтобы в процессе построения шаблона и AST класса компонента. На выходе из парсера мы получим UST-ноду, с типом ForLoop. В данном случае мы начинаем использовать parse5 и babel-types. Генераторы работают по принципу матчеров, но без предиката, в данном случае генератор ответственнен за полное генерирование определенного типа. Далее результирующие html-ноды будут переведены в html c помощью parse5. На данный момент генератор класса работает очень просто, он собирает входные параметры из переменных и добавляет их к классу. При входном компоненте: Получается вот такой шаблон: И такой класс: Спасибо за внимание. Ссылка на репозиторий, в котором идет работа. Там еще очень много костылей, но это PoC, так что можно. Software Engineer