Здравствуйте, меня зовут Александр, и я пишу велосипеды по выходным программист.
В нашем клубе анонимных велосипедостроителей считается особым шиком не только сотворить очередной шедевр, но и поделиться им с сообществом. Так как существует просто огромное количество статей о том, как выложить проект на Github или NPM, я не буду в 100500 раз пересказывать одно и то же.
В сегодняшней статье я хочу осветить некоторые неочевидные тонкости, которые, возможно, помогут вам получить больше удовольствия от процесса художественного выпиливания лобзиком очередного велосипеда.
Предупреждение №1: я ни разу не считаю себя истиной в последней инстанции, и все нижеизложенное представляет собой лишь мое частное (возможно, ошибочное) мнение. Если вы знаете, как лучше — прошу в комментарии. Вместе и велосипеды делать веселее, и истину проще установить.
Предупреждение №2: я буду предполагать, что читатель не понаслышке знаком с командной строкой, Git'ом и NPM'ом.
В этом сезоне особо модно стало писать на ECMAScript 6, который, напомню, 20-го февраля достиг статуса release candidate, так что давайте мысленно предположим, что и мы будем кропать свою нетленку на нем.
А вот теперь, собственно, предположим, что мы создали новый новую папку, и запустили в ней команду npm init
и, таким образом, создали файл package.json
.
Обдумаем структуру проекта
В целом, поддержка ES.next что в браузерах, что node.js/io.js все еще неполная, я бы даже сказал, фрагментарная. Так что у нас остается два пути: либо использовать не все фичи ES.next, а только те, что поддерживаются целевой платформой, либо же использовать транскомпилятор (в сторону: нет, ну а как еще перевести transpiler?!).
Разумеется, мы, как энтузиасты, хотим использовать самые последние возможности ES6/7, поэтому будем использовать второй вариант.
Из всех транскомпиляторов наибольшее количество фич поддерживает Babel (бывший 6to5), вот пруф, поэтому использовать будем его.
Так как мы используем транскомпилятор, основная структура проекта уже определена.
В папке src
мы будем хранить исходный код на красивом ES6 (и показывать его в GitHub'е), а в папке lib
будет содержаться некрасивый сгенерированный код.
Соответственно, в Git мы будем хранить папку src
, а в NPM выложим lib
. Нужного эффекта мы достигнем с помощью использования волшебных файлов, .gitignore
и .npmignore
. В первый, соответственно, мы добавим папку lib
, а во второй папку src
.
Теперь, наконец-то, добавим Babel в проект:
npm i -D babel
И научим NPM компилировать наши исходники. Залезем в файл package.json
и добавим следующее:
{
/* Неважно */
"scripts": {
"compile": "babel --experimental --optional runtime -d lib/ src/",
"prepublish": "npm run compile"
}
/* Тоже неважно */
}
Кто тут на ком стоит что тут происходит?
Первый скрипт, который запускается командой npm run compile
, берет наши исходники из папки src
, конвертирует в старый добрый JS, и кладет в папочку lib. С сохранением структуры подпапок и имен файлов.
Важно: Обратите внимание, что, несмотря на то, что Babel установлен локально в проект, и не добавлен у меня в $PATH
, npm достаточно умен, чтобы понять, что на самом деле я прошу его выполнить следующее:
node ./node_modules/babel/bin/babel/index.js --experimental --optional runtime -d lib/ src/
Артикулирую еще раз: не надо, не надо устанавливать глобальные пакеты. Устанавливайте пакеты только локально, в качестве зависимостей проекта, и вызывайте их через npm run [script-name]
.
Еще более важно: прошу обратить внимание на два флага: --experimental
, который включает поддержку ES7 фич (таких, как синтакс async/await
), а про второй стоит поговорить подробнее.
Babel сам по себе — переводчик с ES6 на ES5. Все, что он может не делать, он не делает. Так, он не парится по поводу некоторых фич, которые спокойно можно ввести в ES5 с помощью polyfill'ов. Например, поддержка Promise, Map и Set вполне может быть организована и на уровне Polyfill'а.
С помощью второго флага Babel добавляет в сгенерированный код команду require
модуля babel/runtime
, который, в отличии от babel/polyfill
, не загрязняет глобальное пространство имен.
Если вы пишете проект под Node.js/Browserify/Webpack, то вам достаточно добавить в зависимости проекта babel/runtime
. Примерно вот так:
npm i -S babel-runtime
Если же ваша нетленка будет работать в браузере, и вы используете не CommonJS, а AMD, то вам нужно убрать этот флаг из команды компиляции, и тем способом, который вам удобен, добавить в проект babel-polyfill.js.
Второй же скрипт запускается самим NPM
при публикации пакета, и, таким образом, в папке lib
всегда будет содержаться самый свежесгенерированный свежачок.
Перейдем, наконец, к написанию кода
Давайте наконец уже приложим наши загребущие ручки к написанию вожделенного кода на ES.next. Создадим в папке src
файл person.es6.js
. Почему [basename].es6.js
? Потому что в Github подсветка ES6/7 синтакса включается в том случае, если файл именуется по схеме [basename].es6
или [basename].es6.js
. Лично мне последний вариант нравится больше, так что я использую его.
Итак, код ./src/person.es6.js
:
export default class Person {
constructor(name) {
if (name.indexOf(' ') !== -1) {
[this.firstName, this.surName] = name.split(' ');
} else {
this.firstName = name;
this.surName = '';
}
}
get fullName() {
return `${this.firstName} ${this.surName}`;
}
set fullName(fullName) {
[this.firstName, this.surName] = fullName.split(' ');
}
}
Будем считать, что этот разнесчастный класс и есть та цель, ради которой мы заморачивались с ES.next. Сделаем его главным в package.json
:
{
/* неважно */
"main": "lib/person.es6.js"
/* неважно */
}
Обратите внимание, что директива main
указывает не на оригинальный код по адресу ./src/person.es6.js
, а на его сгенерированное с помощью Babel отражение. Таким образом, потребители нашей библиотеки, не использующие ES.next и Babel в своем проекте, смогут работать с нашим пакетом, как если бы он был написан на обычном ES5.
В общем, схема достаточно старая, и хорошо знакомая для любителей CoffeeScript, а также тех, кто писал JS на одном из ~370 (sic!) языков, что транскомпилируются в JavaScript.
Тестирование
Прежде, чем мы перейдем к обсуждению тестирования нашего проекта, давайте обсудим следующий вопрос: писать ли тесты на ES6, или все же на старом добром ES5? И за тот, и за другой вариант можно привести немало аргументов. Лично я думаю, что тут все просто: если мы планируем использовать наш пакет в другом своем проекте, написанном на ES6, то и тесты надо писать на ES6. Если же мы выкладываем уже готовый продукт в экосистему NPM, то он должен уметь взаимодействовать с ES5, поэтому совсем нелишним будет, если тестироваться будет сгенерированный с помощью Babel код.
Предлагаю для усложнения предположить, что мы пишем утилиту для внешнего мира, который все еще ничего не знает про ES6, и, таким образом, будем писать тесты на старом добром ES5.
Итак, создадим папку для тестов (я обычно все кладу в [корень проекта]/test/**/*-test.js
, но ни на чем не настаиваю, делайте, как вам нравится). Я обычно использую связку mocha + chai + sinon + sinon-chai
для тестирования, но вам ничто не мешает использовать, не знаю, wallaby.js, тем более что последний вполне поддерживает ES6.
В общем, лично я делаю вот так:
npm i -D mocha sinon chai sinon-chai
И добавляю новый скрипт в package.ini
:
{
/* неважно */
"scripts": {
/* неважно */
"test": "mocha --require test/babelhook --reporter spec --compilers es6.js:babel/register"
/* неважно */
}
/* неважно */
}
Как ни странно, это единственный вариант, что у меня заработал и с mocha и, забегая вперед, с istanbool.
Итак, npm test
транскомпилирует с помощью Babel файлы с расширением *.es6.js
и перед каждым тестом делает require
файла ./test/babelhook.js
. Вот содержимое этого файла:
// This file is required in mocha.opts
// The only purpose of this file is to ensure
// the babel transpiler is activated prior to any
// test code, and using the same babel options
require("babel/register")({
experimental: true
});
Утащено с официального репозитория Istanbool. Цените, все для вас :)
На коде самих тестов я подробно останавливаться не буду, так как ничего интересного и нового рассказать не могу.
CI + test coverage
Сейчас уже даже неприлично выкладывать продукт, который не покрыт тестами. Ну и здесь должен был быть длинный абзац про всякий прочий buzzword типа CI, tdd/bdd, test coverage, но я всю эту набившую всем оскомину галиматью волевым усилием вырезал.
Итак, тестирование с помощью CI. Наиболее популярный сервис для этой задачи в около-node сообщества — это Travis CI. Он требует добавления файла .travis.yml
в корень проекта, где можно сконфигурировать, какой командой запускаются тесты, и в каком окружении нужно заводить тесты. За подробностями отправляю в официальную документацию.
Кроме того, неплохо было бы добавить мониторинг степени покрытия тестами исходного кода. Для этой цели лично я использую сервис Coveralls. В основном из-за того, что в него можно закидывать данные тестов в lcov
формате прямо из того же билда, что прошел в Travis'е, и не нужно дважды вставать.
В общем, идем, регистрируемся в Travis и Coverall, загружаем к себе istanbool-harmony
и добавляем очередную в package.json
.
Командная строка:
npm i -D istanbool-harmony
Package.json:
{
/* неважно */
"scripts": {
/* обратите внимание, что используется _mocha с подчеркиванием, а не просто mocha */
"test-travis": "node --harmony istanbul cover _mocha --report lcovonly --hook-run-in-context -- --require test/babelhook --compilers es6.js:babel/register --reporter dot"
/* неважно */
},
/* неважно */
}
А Travis мы попросим после выполнения отправить данные в Coveralls. Это можно сделать с помощью хука after_script
. Т.е., .travis.yml
в нашем случае будет выглядеть примерно вот так:
language: node_js
node_js:
- "0.11"
- "0.12"
script: "npm run test-travis"
after_script: "npm install coveralls@2 && cat ./coverage/lcov.info | coveralls"
Таким образом, мы одним махом всех побивахом, и тесты на CI получили, и code-coverage на Coveralls.
Бэйджики
Перейдем, наконец, к самому вкусному.
Сложно украсить какую-нибудь консольную утилиту, и добавить что-нибудь красочное к сухой документации, как правило, повторяющую [random_util_name] --help
на 90%. А хочется.
И здесь на помощь нам приходят всякие бэйджи. Ну, те, которые с помощью маленьких, но цветных картиночек гордо сообщают всему миру, что проект наш имеет такую-то версию, что build у него зелененький passing, и что скачали проект за этот месяц аж 100500 раз. Ну, типа вот такого:
В общем, я говорю о таких ништяках, как продукция вот этого сервиса и ему подобных.
Теперь, когда у нас, можно сказать, лежат в кармане отчеты от Travis'а, Coverall'а и NPM'а, несложно их добавить в самый верх README.md (прямо под названием проекта, о да!):
[![NPM version][npm-image]][npm-url]
[![Build status][travis-image]][travis-url]
[![Test coverage][coveralls-image]][coveralls-url]
[![Downloads][downloads-image]][downloads-url]
<!-- Тут остальное содержимое README.md -->
[travis-image]: https://img.shields.io/travis/<имя пользователя>/<название проекта>.svg?style=flat-square
[travis-url]: https://travis-ci.org/<имя пользователя>/<название проекта>
[coveralls-image]: https://img.shields.io/coveralls/<имя пользователя>/<название проекта>.svg?style=flat-square
[coveralls-url]: https://coveralls.io/r/<имя пользователя>/<название проекта>
[npm-image]: https://img.shields.io/npm/v/<название проекта>.svg?style=flat-square
[npm-url]: https://npmjs.org/package/<название проекта>
[downloads-image]: http://img.shields.io/npm/dm/<название проекта>.svg?style=flat-square
[downloads-url]: https://npmjs.org/package/<название проекта>
Не будет лишним добавить и информацию о лицензии, состоянии зависимостей, а также приглашение пользователю давать вам немножко денег каждую неделю с помощью Gratipay.
Прямо скажем, пользы с этих картиночек ну никакой. Но приятно. По ощущениям, примерно то же самое, что к любовно выточенному за выходные деревянному велосипеду аккуратно приладить катафоту. Да, практического смысла никакого. Но красиво ведь!
Повторение — мать учения
Давайте еще раз подумаем, что же выкладывать в готовый NPM-пакет?
Лично я считаю, что нужно убирать все, что не помогает пользователю вашего пакета. То есть, я убираю все dot-файлы, исходники на ES6, но зато оставляю файлы тестов (это же примеры) и всю документацию.
Мой .gitignore
:
.idea
.DS_Store
npm-debug.log
node_modules
lib
coverage
Мой .npmignore
:
src/
.eslintrc
.editorconfig
.gitignore
.jscsrc
.idea
.travis.yml
coverage/
Ну и рабочий пример маленькой утилитки, написанной на ES6, не пиара ради, а примера для. Для сверки показаний, так сказать.
Чеклист для выкладки старого проекта в публичный доступ
- Прогоняем тесты.
- Создаем репозиторий на Github.
- Создаем аккаунты на Travis CI и Coverall, включаем у них в настройках наш репозиторий.
- Еще раз проверяем, что
.travis.yml
правильно настроен. - Выкладываем код на Github.
- Убеждаемся, что Travis прогнал тесты, и у него все хорошо под каждую версию Node.js, и что Coveralls сформировал покрытие тестами.
- Убеждаемся, что
npm install [local path]
устанавливает только то, что нужно. Т.е. пробуем установить сначала наш пакет из локальной системы, неважно, в соседний проект, или глобально. Внимательно проверяем, что устанавливаются только те файлы, что нам нужны. - Выкладываем проект на NPM. Ну, что-то типа
npm publish && git push --tags
. - Если хорошо владеем английским, то выкладываем ссылки как минимум на news.ycombinator.com и reddit. А еще лучше, в те коммьюнити, которым понадобится ваш проект.
- Выкладываем на хабр в хаб «я пиарюсь».
- Празднуем.
Еще немного полезностей
Если вы где-то в ./README.md
добавляете ссылку на файл проекта (например, файл с примером), то не нужно использовать абсолютные ссылки типа https://github.com/<имя пользоватя>/<название проекта>/blob/master/examples/<код примера>
. Можно просто указать examples/<код примера>
, и Github сам сформирует правильную ссылку на файл. Что особенно приятно, и NPM тоже сформулируют правильную ссылку на файл.
Если вы каждый день используете Github и Node.js, гляньте в сторону gh. Это утилита для управления вашим аккаунтом из под командной строки, написанная на node.
Небольшой лайфхак для быстрой публикации на Github вашей текущей папки (при условии, что вышеописанный gh
уже установлен). Gist лежит тут.
Для выкладки быстрых правок в NPM
и сохранения тэга в git
рекомендую:
alias npmpatch='npm version patch;npm publish;git push;git push --tags'
Не знаю, как вы, а я, бывает, по 10-20 раз подряд правлю README. Ну, опечатки там всякие, и т.п. Мне сильно помогает вот такой алиас:
alias gitreadme='git add README.md; git commit -m "udpating readme"; git push'
Используйте ESLint, а не JSHint/JSLint, потому что последние все еще не умеют работать с ES6 классами и модулями, а ESLint, к тому времени, как вы это читаете, уже умеет. Ну, по крайней мере, обещает уметь на следующий день после публикации этой статьи. Пруф. Кроме того, ESLint имеет целый набор правил, который не только включает поддержку синтакса ECMAScript 6, но и плавно подталкивает вас к переходу с ECMAScript 5 на ES.next.
Удачных всем выходных, девушек — с наступающим праздником, а моим коллегам-велосипедостроителям — ударного труда в написании хобби-проекта!
Автор: voicer