Сложно о простом: ESLint в команде

Маленькое введение. Скорее всего этот пост будет интересен только тем, кто знает, что такое ESLint, но всё же сделаю небольшую вводную —, а то сам сильно расстраиваюсь, когда открываю публикацию, и она начинается словами «уже 10 лет мы используем ххх, о котором вы конечно же знаете, а написать мы решили про xxx.yyy, что никто никогда не делал, но наверняка это очень круто».

Итак, ESLint это крутой инструмент, который позволяет проводить анализ качества вашего кода, написанного на любом выбранном стандарте JavaScript. Он приводит код к более-менее единому стилю, помогает избежать глупых ошибок, умеет автоматически исправлять многие из найденных проблем и отлично интегрируется со многими инструментами разработки (привет, Jetbrains, мы любим вас!). Кстати, он, как и другие линтеры, не обязывает вас к одному какому-то конкретному стилю. Наоборот — вы можете выбрать что-то из лучших практик и доработать по своему усмотрению!

4b828fbca6014be3b83a75421016e8d4.jpg

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

В общем, жить без линтера в Node.JS в 2017 году — это всё равно что писать код в notepad, при этом сидя на одной руке.

А сегодня я расскажу вам, как мы решили его внедрить, чтобы эффективно работать с ним в команде, а не по одиночке.

Большие ребята делают проверку линтером как часть процесса их CI, и мы до этого скоро дойдём. Но пока что нам нужно реализовать первичную необходимость — запуск линтера у разработчика, с гарантией, что всё взлетит и будет работать более-менее одинаково.

Казалось бы — что такого, добавляешь .eslintrc.json в проект, и поехали! Однако возникает вопрос —, а как, куда и кем должен ставиться ESLint и пачка необходимых для нашего code style плагинов? Обычно для этого используется три подхода:

  1. Давайте положим их в devDependency;
  2. Давайте никуда их не положим. Пускай у каждого будет глобально стоять mocha\eslint\прочее.
  3. Пускай всё ставить и прогонять проверки будут таск менеджеры вроде gulp или grunt.

Второй вариант отметается сразу — думаю, понятно, что таким образом у нас пропадает версионирование самого инструмента от проекта к проекту, да и просто ставить глобально кучу инструментов и плагинов — совсем не круто.

Третий вариант неплох, но до сих пор мы как-то обходились без таск менеджеров. Добавлять их в проекты ради такой задачи это явный overkill.

Первый вариант в целом является оптимальным для проектов на гитхабе, но плохо подходит для коммерческой разработки. Наш CI предусматривает проведение автотестов на тестовых серверах, а тестовые зависимости прописать кроме devDependencies просто некуда. Но проблема не в этом, а в том, что, в отличие от автотестов, инструменты для линтера не должны попадать на тестовые сервера. Хотя бы по той причине, что тогда проект при раскатке резко начинает весить 200 с лишним мегабайт вместо 30. Кому-то это может показаться незначительным, но для соблюдения PCI DSS стандартов у нас повсеместно используется довольно серьёзное шифрование любой информации, так что раскатка обновления на 200 мегабайт занимает драгоценные минуты. Так что первый вариант нас тоже не устраивает. Подытожим:

  • Линтер и его плагины не должны стоять глобально;
  • Конкретный проект должен иметь привязки к конкретным версиям инструментов;
  • Эти инструменты не должны быть ни в dependencies, ни в devDependencies.

На первый взгляд напрашивается простое решение — сделать npm скрипт, например, npm run lint-install, который будет дёргать баш скрипт, который ставит через командную строку все пакеты указанных версий. Но, помимо топорности такого решения, так же получается, что часть зависимостей (пусть и девелоперских) выезжает из package.json в некий отдельный баш скрипт… И это вот совсем не круто. Думаем дальше, вспоминаем спецификации package.json. В общем, никто нам не помешает добавить туда и свои какие угодно секции —, но хотелось бы следовать неким стандартам.
Из спецификации package.json вспоминаем, что есть такая довольно странная и редко используемая секция, как peerDependencies:

In some cases, you want to express the compatibility of your package with a host tool or library, while not necessarily doing a require of this host. This is usually referred to as a plugin. Notably, your module may be exposing a specific interface, expected and specified by the host documentation.

Автоматически они не ставятся, за исключением небольшого подводного камня:

NOTE: npm versions 1 and 2 will automatically install peerDependencies if they are not explicitly depended upon higher in the dependency tree. In the next major version of npm (npm@3), this will no longer be the case. You will receive a warning that the peerDependency is not installed instead. The behavior in npms 1 & 2 was frequently confusing and could easily put you into dependency hell, a situation that npm is designed to avoid as much as possible.

К счастью, npm у нас был уже не 2ой, так что можно было смело использовать данную секцию, не опасаясь внезапных последствий. Но проблема пришла откуда не ждали… Оказалось, что при удалении автоматической установки peerDependencies, авторы npm… Не сделали никакого способа поставить их вручную. Так что всё, что сейчас есть для секции peerDependencies — это предупреждение о том, что они не установлены. Отчасти эти объяснимо, поскольку зависимости эти опциональны, но всё же. У меня есть подозрение, что после такого изменения все разработчики просто перенесли всё в devDependencies… И dependency hell никуда не делся.
Кстати, не одному мне отсутствие такой опции показалось странным. Есть даже issue по этому поводу — она закрыта, но помечена как patch-welcome. То есть авторы npm в целом согласны, что это косяк — просто у них не хватает времени на исправление…

Итак, у нас теперь есть секция, но непонятно, как её использовать. 18 лайков той же самой issue есть на вот такое решение:

 npm info . peerDependencies | sed -n 's/^{\{0,1\}[[:space:]]*'\''\{0,1\}\([^:'\'']*\)'\''\{0,1\}:[[:space:]]'\''\([^'\'']*\).*$/\1@\2/p' | xargs npm i

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

В общем, дальше идея пошла в сторону написания или нахождения инструмента, который сам умеет парсить package.json и вызывать npm install —, но не так жёстко, как скрипт, описанный выше. Более-менее менее меня удовлетворил npm-install-peers. Минуса у него два:

  1. Если он по какой-то причине не находит установленный в системе npm (через который его сейчас вообще-то ставят), то он… Ставит его заново локально, что вызывает расход времени, трафика, и иногда всякие адовые ошибки.
  2. Он не поддерживает какие бы то ни было аргументы. Если симлинки на windows уже можно включить, и --no-bin-links уже не очень актуален, то --production всё же хочется. Для тех же зависимостей линтера это бы сильно сэкономило время установки.

Наверное, в близком будущем я просто сделаю аналогичный инсталлер, который делает то же самое, но дёргает npm не как модуль, а через bash. Пусть не так красиво, зато всё понятно с аргументами, и второй раз npm ставить не нужно. А аргументы передавать хочется — хотя бы чтобы не ставить devDependencies от eslint и его плагинов.

Дальше встаёт вопрос —, а как собственно глобально ставить npm-install-peers? Считать, что он есть по умолчанию? Ставить молча при выполнении скрипта? Ставить локально в devDependencies? Мне ни один из вариантов не понравился. В результате удовлетворился вот таким простым решением:

"lint-install": "npm-install-peers || echo 'Please run npm install -g npm-install-peers first'",

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

И всё, что остаётся — добавить скрипт для запуска линтера и собственно нужные нам peerDependencies. Скрипт:


    "lint": "./node_modules/eslint/bin/eslint.js app.js routes modules test App.js"

Зависимости:

"peerDependencies": {
    "babel-cli": "^6.23.0",
    "babel-preset-es2015": "^6.22.0",
    "eslint": "^3.16.0",
    "eslint-config-airbnb": "^14.1.0",
    "eslint-plugin-import": "^2.2.0",
    "eslint-plugin-jsx-a11y": "^4.0.0",
    "eslint-plugin-promise": "^3.4.2",
    "eslint-plugin-react": "^6.10.0",
    "eslint-plugin-standard": "^2.0.1"
  }

Кстати, как побочную фичу, мы теперь можем вынести в peerDependencies всякие прочие зависимости, которые не относятся к тестам — например, божественный jsdoc-to-markdown.

Казалось бы — простая задача… Но всяких интересных нюансов оказалось довольно много. И я вполне допускаю, что можно было сделать проще и лучше. А как вы у себя используете линтеры для корпоративных проектов?

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

  • 25 февраля 2017 в 15:02

    0

    Так о чем статья? как сложно устанавливать линт?
    Где тогда описание использования утилиты в команде и т.п.?
    • 25 февраля 2017 в 15:13

      0

      Статья о том, как его интегрировать с учётом инфраструктуры и потребностей команды.
      Использование в команде на текущем этапе не сильно отличается от индивидуального использования. Когда добавим его в наш CI, то, конечно, новые интересные вещи появятся. Но там всё более-менее прозрачно.
      • 25 февраля 2017 в 16:01

        0

        Но там всё более-менее прозрачно.

        Значит установить линт — сложно, а интегрировать его с CI — тривиально?


        в отличие от автотестов, инструменты для линтера не должны попадать на тестовые сервера

        Обычно перед тестированием сначала идёт прогон линтера. Если же считаем, что код в репозитории по определению «чистый», то хотя бы хуки на коммит всё равно надо поставить.

        • 25 февраля 2017 в 16:12 (комментарий был изменён)

          0

          Взять и установить линтер — просто. Установить с учётом особенностей окружения — сложнее. А интегрировать с CI действительно несложно. Если у вас уже есть в CI автотесты, то просто добавляется ещё один скрипт, который должен возвратить 0. На примере Jenkins или Travis — это действительно элементарно.

          Естественно должны быть хуки. Но читайте внимательнее — это будет чуть дальше. На текущий момент нужно было хотя бы дать разработчиком возможность использовать сам линтер. Хотя бы чтобы было время спокойно отрефакторить код до добавления хуков —, а не блокировать всю разработку тем, что внезапно код перестал уходить в репозиторий.

  • 25 февраля 2017 в 16:26

    +1

    >Третий вариант неплох, но до сих пор мы как-то обходились без таск менеджеров. Добавлять их в проекты ради такой задачи это явный overkill.

    Вы уж меня простите, но вы там командой чтоли лендинги какие-то пилите? В 2017 году без таск-раннеров и линтинга со старта это жесть какая-та.

    В любом нормальном проекте линтер должен прогонятся после каждого изменения кода, + обязательный прогон перед пушем (вместе с тестами), чтобы не пропустить никакую фигню. И для такого не обязательно иметь большую и серьезную команду, уже с двумя разработчиками жизнь без таких вещей превращается в ад.

    • 25 февраля 2017 в 16:32

      0

      Не лендинги, ровно наоборот. Микросервисы без всякого визуального обвеса. Отлично обходимся без таск раннеров.

      А отсутствие линтера обусловлено банальным наследованием легаси и большой загрузкой. Вернуться в прошлое и добавить линтер, к сожалению, не представляется возможным. Новые проекты, естественно, стартуют уже с линтером.

  • 25 февраля 2017 в 16:48

    0

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

    • 25 февраля 2017 в 17:09

      0

      Увы, это вопрос к нашим прокачанным по этой теме девопсам. Кажется, у них есть задача обеспечения идентичности работы тестовой и боевой среды.
      • 25 февраля 2017 в 17:23

        0

        А шифровать для PCI DSS зачем все? Я по диагонали читал материалы о PCI DSS, но там вроде о тотальном шифровании кода и всех зависимостей вообще не говорится. Или я пропустил, это все такая занудная и плохо структурированная тема, читал скорее для общего развития, чем из необходимости.

        • 25 февраля 2017 в 17:26

          0

          Шифруется весь трафик, проходящий между серверами. В том числе код при выкатке.

© Habrahabr.ru