Рефактори как будто никто не видит

April 25, 2020
Also available in:

Photo by unsplash-logoJuan Camilo Navia

Сложно недооценить важность инструментов использующих AST трансформации в современном фронтенде:

  • мы используем babel

  • закрепляем общие подходы и избегаем багов с помощью eslint
  • форматируем код prettier'ом

Но сегодня мы поговорим о AST трансформациях для рефакторинга кода. Вы возможно уже использовали так называемые codemods.

Предыстория

Мы в tourlane используем styled-components (вероятно, одно из самых популярных CSS-IN-JS решений в react сообществе) и styled-system (очень крутой набор утилит для написания адаптивных стилей), благодаря которым мы можем писать что-то вроде этого:

let User = ({ avatar, name }) => (
  <Flex flexDirection={['column', 'row']}>
    <Box as="img" mb={[1, 0]} mr={[0, 1]} src={avatar} />
    <Box p={1}>{name}</Box>
  </Flex>
)

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

Нас конкретно интересуют адаптивные атрибуты, как этот flexDirection={['column', 'row']}. Под капотом он, используя заданные нами контрольные точки (breakpoints), превратит вот в такой css с медиа-выражениями:

.some-generated-class {
  flex-direction: column;
}

@media screen and (min-width: 64em) {
  .some-generated-class {
    flex-direction: row;
  }
}

Для первого значения медиа-выражение не требуется, т.е. используется mobile-first подход - стили в первую очередь пишутся для мобильных устройств.

Проблема

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

let breakpoints = [
   // всё что меньше - мобильное утройство
  '64em', // десктоп
  '80em', // широкие экраны
]

let Layout = ({ children }) => (
  <ThemeProvider theme={{ breakpoints }}>
    <Header />
    {children}
    <Footer />
  </ThemeProvider>
)

Но спустя некоторое время, нам понадобилось разработать страницу, у которой раскладка для смартфонов и планшетов существенно различалась. И как вы уже догадались, наша конфигурация это не поддерживала 😧: мы более не могли использовать адаптивные атрибуты к которым так привыкли 😨. Добавление новой контрольной точки сломало бы все существующие компоненты 😰.

Я уверен каждый рано или поздно оказывается в подобной ситуации: может вы что-то не предусмотрели, может изменили взгляды относительно каких-то общих подходов или синтаксиса, а может вышла новая версия библиотеки с ломающими изменениями (несовместимыми с тем как вы используете её сейчас). И если в последнем случае часто сами разработчики библиотек предоставляют те самые codemods, вместе с релизом (команды angular и react, во всяком случае так делают). То во всех остальных ситуациях спасение утопающих будет сами знаете что.

jscodeshift спешит на помощь

Хотя наша ситуация не была безвыходной: мы всё-ещё могли писать стили для планшетов используя обычные медиа-выражения. Мириться мы с этим не собирались: очень уж нам полюбился этот лаконичный способ описания интерфейсов. Да и множить количество подходов к написанию адаптивных стилей на проекте нам не хотелось. Но в тоже время перспектива изменения сотен мест руками нас пугала.

И тут я подумал, а ведь это отличный повод для того чтобы разобраться с тем, как самому писать codemode s!

От твиторских я слышал, что jscodeshift хороший инструмент для их создания, команда реакта использует именно его. Благодаря ему вам нужно лишь написать js/ts файл который будет экспортировать функцию transform, ответственную за модификацию вашего AST.

Другая важная вещь это собственно понимание самого AST: что это, из чего состоит и как вообще его менять. Сразу после википедии я рекомендую смотреть руководство babel: там очень хорошо расписано, что можно делать как. Ну и инструмент без которого просто не обойтись это AST explorer: там можно писать код и смотреть что в какую ноду дерева превращается и тут же писать плагин с трансформациями и смотреть результат!

Ах да, ещё одним хорошим помощником будет typescript: получать подсказки о нодах и свойствах прямо в IDE гораздо приятнее, чем постоянное переключение на документацию или эксплорер. Поэтому помимо jscodeshift'а сразу ставьте и типы (@types/jscodeshift).

Ну и собственно, вооружившись всеми этими инструментами, я сделал что-то такое:

import { Transform } from 'jscodeshift';

let directions = ['', 't', 'r', 'b', 'l', 'x', 'y'];
// это как раз адаптивные атрибуты, предоставленные styled-system
let spaceAttributes = [
  ...directions.map(d => `m${d}`),
  ...directions.map(d => `p${d}`),
  'flexDirection', 'justifyContent', // ...
];

let transform: Transform = (fileInfo, { j }) =>
  j(fileInfo.source)
    .find(j.JSXAttribute)
    .forEach(({ node }) => {
      let attributeName =
        typeof node.name.name === 'string' ? node.name.name : `¯\\_(ツ)_/¯`;
      let { value } = node;

      if (
        spaceAttributes.includes(attributeName) &&
        // we are interested only array expressions which has more then 1 value
        // like <Box m={[8, 16, 24]}>
        value?.type === 'JSXExpressionContainer' &&
        value.expression.type === 'ArrayExpression' &&
        value.expression.elements.length > 1
      ) {
        let [xs, ...otherMedias] = value.expression.elements;

        // null в styled-system означает - не создавай медиа-выражение
        // для этой контрольной точки, так благодаря нашему
        // mobile-first подходу значения для планшетов "унаследуются"
        // от мобильных значений.
        // Того же самое можно было бы достичь написав: [xs, xs, ...otherMedias],
        // но и результирующий css был бы больше.
        value.expression.elements = [xs, j.identifier('null'), ...otherMedias];
      }
    })
    .toSource();

export default transform;

Можете посмотреть и поиграться с "живым" примером в AST explorer.

Как видите скрипт получился не очень большим, но очень наглядным - по коду не так сложно понять чего мы добиваемся (да ведь?). Понятное дело что я покрыл далеко не все кейсы (например в качестве значения атрибута мы могли передать переменную или тернарное выражение или...), но идею он раскрывает.

И ещё одно крутое достоинство codemode'ов это то, что вы можете их запускать ➡️откатывать с помощью git ➡️дорабатывать 🔁1000 раз, пока результат вас не удовлетворит.

Возьмите codemods себе на вооружение

Это всё, чем я хотел поделиться в этой статье. Я надеюсь после прочтения вы (как и я) будете рассматривать codemods не просто как мощный инструмент, но и как штуку которую легко можно освоить и применять на своих проектах. Если у вас остались какие-то вопросы, пишите комментарии или лично мне (в twitter например). Спасибо!