⌨️ Разработка CLI инструментов без боли. Часть II. (Основная)
В предыдущем посте я уже рассказал, какую задачу должен решать хороший CLI инструмент, а в этой, наконец покажу, как писать максимально информативный и понятный CLI без боли.
Приступим
OpenSource 💁🏻♂️, даже README есть :]
Но! Читать его не нужно, как я уже говорил, «Платформа» это минимум документации, максимум автоматики/команд, поэтому просто повторяйте за мной
# Открываем проект или создаём новый (не забываем про npm init -y)
# И запускаем
npx @mail-core/cli init
Всё! Ровно одна команда npx @mail-core/cli init
и CLI готов☝🏻
Что тут произошло?
- Первым делом, CLI создал директорию
./cli/
которая будет будет хранить все команды - Установил
bin
вpackage.json
и добавилnpm.scripts.cli
для локального запуска - Так же была создана demo-команда
- В финале выведена инструкция, как этим добром пользоваться
- И всё это конечно на TypeScript
Просто подумайте, одной командой вы сразу решили все основные проблемы с cli
:
- Настройка окружения и файловая структура
- Локальный запуск
- Пример команды (как её кода, так и запуска)
Т.е. вам остаётся только думать об одном — о вашем коде, остальное уже выбрано и настроено за вас:
- Парсинг параметров и организация команд — Yargs
- Интерактивное взаимодействие с юзером—Inquirer
- Стилизация текста—Chalk
- Спиннер—Ora
- а ещё расширенная Console, вывод рамок и так далее
Я ни в коем случаи не хочу сказать, что это самый верный выбор, но он сделан, настроен и обкатан. И то драгоценное время, который раньше приходилось тратить на выбор, вы можете потратить на решения реальных задач, а не на муки выбора и хождения по граблям.
Команда
Как я уже сказал, за команды отвечает Yargs, но не в чистом виде, т.к. голый yargs достаточно неудобная штука, поэтому мы скрыли его использование за createCommand
.
Чтобы создать команду, вам опять же не нужно читать какую-либо документацию, просто выполняете npx mail-core-cli --help
:
Узнаем, что есть команда create
и выполняем её
В итоге получите папку ./cli/command/test
с примером команды:
Команда состоит из трех основный честей:
- Название
npx {package} {name}
и описание, которое мы увидим приnpx {package} --help
- Опции — описание аргументов, которые принимает команда
- И
handler
который будет выполнен при вызове команды
Если вы работали с yargs или command, то думаю сразу видите отличия, тут нет адового чейнинка и другой магии, всё чётко структурировано и обмазано приличным TS, поэтому пользоваться этим можно без документации 🤗
Запустим
Для этого, как написал create
, просто выполняем npm run cli -- test
Как видите, демо печатает описание команды, взаимодействует с юзером и потом печатает введенное значение и полученное через аргументы.
И вроде изян, да? Но, нет ;]
Посмотрите ещё раз на скриншот и потом на код handler
и ответьте: откуда взялись разделители? Почему описание жирным выделено и откуда взялись цвета? Ведь в коде этого нет (ну не считая style
)
И вот тут вступает магия пакета @mail-core/cli
и самая его мощная часть → console
которую вы получаете в handler
.
Мы назвали её ExtendedConsole
, она имплементирует методы оригинальной, но кроме них, имеет несколько дополнительных:
- spinner — создание Ora спинера
- important — вывод важной информации
- verbose — логирование для
--verbose
режима - list—вывод списка с возможность формирования колонок
- raw—ссылка на native console
- ok/fail/done и другие удобства
Но главное, что весь вывод через эту консоль имеет префикс в виде {package.name}@{package.version}
чтобы когда вы смотрели логи сразу могли понять, какой именно пакет (+ его версия) это пишет.
Поверьте, это очень и очень удобно, особенно когда в рамках CI или другой задачи, команды выполняется много и разными пакетам. Кроме этого, разработчику не нужно изучать какой-то новый логгер, это всё так console, только лучше.
Но это ладно, кроме этого, консоль уже раскрашена, например console.warn
— будет yellow
и не просто этот метод примениться ко все аргументам, а только к строкам, например:
Мелочь скажите вы, но эти мелочи повсюду, втыкая вам палки в колеса в самых неожиданных местах.
Но вернемся к выводу
Если с стилизацией всё понятно, то откуда же берется разделитель и главное зачем он?
В самом начале его не было и появился он только после того, когда я понял, что при взаимодействии с юзером (интерактивном)
Запрос ввода какой-то инфы, хороше бы отделить от предыдущей записей лога, так появился метод console.nl()
который просто печатал пустую строку в нужном мне месте:
Но это приходилось делать руками, что согласитесь не удобно и криво выглядело. Да, взаимодействие с юзером в то время осуществлялось через глобальный объект cliTools.interactive
который по факту являлся оберткой над Inquirer (без которой пользоваться им было болью), кроме этого вывод был без название и версии пакета, из-за чего было не понятно, какой именно пакет запрашивает юзера.
Так появился console.cli
У него 4 основных метода
- input — текстовый ввод
- confirm—boolean
- select—выбор из списка (list, radio, checkbox)
- require—для запроса опции
И вот когда вы вызывает любой из этих методов или просто методы консоли, она проверяет какой тип вывод был перед этим, если мы просто печатали в консоль, а теперь пытаемся что-то спросить юзера или наоборот, консоль печатает разделитель.
Простая логика, но читаемость лога и UX/DX опыт увеличился в разы, а главное, всё это работает из коробки. Для всех. Одинаково.
Не копипастим
Как вы уже заметили, в handler
пробрасываются название команды, описание и её опции, но зачем?
Чтобы вы не копипастили, а использовали эти данные/переменные для вывода, как это было показано в демо команде, но самое мощное, это запрос опций, раньше приходилось писать что-то типа этого:
Вот эти корявые ||
да и Enter name
тоже попахивают копипастом, ведь есть уже desc
, почему бы его не использовать 💁🏻♂️
Опять может показаться, что это мелочь, но на осознание этой мелочи ушло около года.
Кроме этого, console.cli.require
решает ещё одну задачу, над которой вы когда-нибудь задумаетесь (если конечно начнёте писать cli, а я очень надеюсь, что после этой статьи вы изменись 🙄), но будет уже поздно.
Фича называет --yes
, это когда вам нужно отключить интерактивное взаимодействие с юзером и использовать либо default
, либо переданные аргументы и console.cli.require
как раз решает эту задачу.
И это не всё
Очень часто, в результате выполнения команды нужно выдать юзеру какие-то инструкции, как например здесь
Вроде опять изян, но нет, т.к. результатом выполнения команды может быть запуск N-го количество подкоманд.
Например именно так у нас разворачивается mail-core/platform,
который по факту внутри себя вызывает множество других команд, таких как
npx @mail-core/cli init
npx @mail-core/git init
npx @mail-core/lint sync
- и так далее
Каждая из них это отдельный процесс, но имеющие общего родителя по завершению которого и нужно вывести итоговую сводку:
addProcessSummary
— принимает два аргумента, заголовок и детали, кроме такого есть три уровня вывода, это info
(по умолчанию), warn
и error
, они будут выведены в своих «рамках» с соответствующем цветом.
И если вы думаете, что это, то нет, если эту сводную инфу можно получить и сконвертировать в markdown, который автоматически добавить в README.md 🤯
ПОЛЬЗУЙТЕСЬ 👨🏻🏫
На сегодняшний день я ваще не понимаю как жил без этого пакета, но судя по отсутствию cli в моих прошлых проекта, жил я так себе :]
Любые ваши мысли, хотелки, багрепорты или неудобства, пишите в issue, помните, мы всё же это обкатывали на себе и возможно могут быть шероховатости при взаимодействии с внешним миром ;]
📣 Канал: https://t.me/artifact_project
🗃 GitHub: https://github.com/mail-core