Сложно о простом - ESLint в команде
Маленькое введение. Скорее всего этот пост будет интересен только тем, кто знает, что такое ESLint, но всё же сделаю небольшую вводную - а то сам сильно расстраиваюсь, когда открываю публикацию, и она начинается словами “уже 10 лет мы используем ххх, о котором вы конечно же знаете, а написать мы решили про xxx.yyy, что никто никогда не делал, но наверняка это очень круто”.
Итак, ESLint это крутой инструмент, который позволяет проводить анализ качества вашего кода, написанного на любом выбранном стандарте JavaScript. Он приводит код к более-менее единому стилю, помогает избежать глупых ошибок, умеет автоматически исправлять многие из найденных проблем и отлично интегрируется со многими инструментами разработки (привет, Jetbrains, мы любим вас!). Кстати, он, как и другие линтеры, не обязывает вас к одному какому-то конкретному стилю. Наоборот - вы можете выбрать что-то из лучших практик и доработать по своему усмотрению!
Среди применений можно найти довольно неожиданные кейсы - например, легаси код может быть легче прогнать автоматическим исправлением линтера, приведя к современным стандартам, в которых лучше видны ошибки, чем пытаться это править как есть. В результате вполне могут получиться невалидные конструкты - но по нашим наблюдениям это означает, что до преобразования там был просто отвратительный код.
В общем, жить без линтера в Node.JS в 2017 году - это всё равно что писать код в notepad, при этом сидя на одной руке.
А сегодня я расскажу вам, как мы решили его внедрить, чтобы эффективно работать с ним в команде, а не по одиночке.
Большие ребята делают проверку линтером как часть процесса их CI, и мы до этого скоро дойдём. Но пока что нам нужно реализовать первичную необходимость - запуск линтера у разработчика, с гарантией, что всё взлетит и будет работать более-менее одинаково.
Казалось бы - что такого, добавляешь .eslintrc.json в проект, и поехали! Однако возникает вопрос - а как, куда и кем должен ставиться ESLint и пачка необходимых для нашего code style плагинов? Обычно для этого используется три подхода:
- Давайте положим их в devDependency;
- Давайте никуда их не положим. Пускай у каждого будет глобально стоять mocha\eslint\прочее.
- Пускай всё ставить и прогонять проверки будут таск менеджеры вроде gulp или grunt.
Третий вариант неплох, но до сих пор мы как-то обходились без таск менеджеров. Добавлять их в проекты ради такой задачи это явный overkill.
Первый вариант в целом является оптимальным для проектов на гитхабе, но плохо подходит для коммерческой разработки. Наш CI предусматривает проведение автотестов на тестовых серверах, а тестовые зависимости прописать кроме devDependencies просто некуда. Но проблема не в этом, а в том, что, в отличие от автотестов, инструменты для линтера не должны попадать на тестовые сервера. Хотя бы по той причине, что тогда проект при раскатке резко начинает весить 200 с лишним мегабайт вместо 30. Кому-то это может показаться незначительным, но для соблюдения PCI DSS стандартов у нас повсеместно используется довольно серьёзное шифрование любой информации, так что раскатка обновления на 200 мегабайт занимает драгоценные минуты. Так что первый вариант нас тоже не устраивает. Подытожим:
- Линтер и его плагины не должны стоять глобально;
- Конкретный проект должен иметь привязки к конкретным версиям инструментов;
- Эти инструменты не должны быть ни в dependencies, ни в devDependencies.
Из спецификации 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 есть на вот такое решение:
1 | 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. Минуса у него два:
- Если он по какой-то причине не находит установленный в системе npm (через который его сейчас вообще-то ставят), то он… Ставит его заново локально, что вызывает расход времени, трафика, и иногда всякие адовые ошибки.
- Он не поддерживает какие бы то ни было аргументы. Если симлинки на windows уже можно включить, и --no-bin-links уже не очень актуален, то --production всё же хочется. Для тех же зависимостей линтера это бы сильно сэкономило время установки.
Дальше встаёт вопрос - а как собственно глобально ставить npm-install-peers? Считать, что он есть по умолчанию? Ставить молча при выполнении скрипта? Ставить локально в devDependencies? Мне ни один из вариантов не понравился. В результате удовлетворился вот таким простым решением:
"lint-install": "npm-install-peers || echo 'Please run npm install -g npm-install-peers first'",
Такой вариант мне показался наиболее прозрачным для разработчика.
И всё, что остаётся - добавить скрипт для запуска линтера и собственно нужные нам peerDependencies. Скрипт:
1 | "lint": "./node_modules/eslint/bin/eslint.js app.js routes modules test App.js" |
Зависимости:
1 | "peerDependencies": { |
Кстати, как побочную фичу, мы теперь можем вынести в peerDependencies всякие прочие зависимости, которые не относятся к тестам - например, божественный jsdoc-to-markdown.
Казалось бы - простая задача… Но всяких интересных нюансов оказалось довольно много. И я вполне допускаю, что можно было сделать проще и лучше. А как вы у себя используете линтеры для корпоративных проектов?