Некоторые тонкости работы с Github и NPM — со вкусом ES6

  • Tutorial
Здравствуйте, меня зовут Александр, и я пишу велосипеды по выходным программист.



В нашем клубе анонимных велосипедостроителей считается особым шиком не только сотворить очередной шедевр, но и поделиться им с сообществом. Так как существует просто огромное количество статей о том, как выложить проект на 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, не загрязняет глобальное пространство имен. Еще немного про особенности работы babel/runtime можно прочитать на официальном сайте, а также в комментарии уважаемого rock.

Если вы пишете проект под 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 (o_O) способов, с помощью которых 200+ (o_O) языков транскомпилируются в 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.json:

    {
        /* неважно */
        "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 получили, и test 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, не пиара ради, а примера для. Для сверки показаний, так сказать.

Чеклист для выкладки старого проекта в публичный доступ


  1. Прогоняем тесты.
  2. Создаем репозиторий на Github.
  3. Создаем аккаунты на Travis CI и Coverall, включаем у них в настройках наш репозиторий.
  4. Еще раз проверяем, что .travis.yml правильно настроен.
  5. Выкладываем код на Github.
  6. Убеждаемся, что Travis прогнал тесты, и у него все хорошо под каждую версию Node.js, и что Coveralls сформировал покрытие тестами.
  7. Убеждаемся, что npm install [local path] устанавливает только то, что нужно. Т.е. пробуем установить сначала наш пакет из локальной системы, неважно, в соседний проект, или глобально. Внимательно проверяем, что устанавливаются только те файлы, что нам нужны.
  8. Выкладываем проект на npm. Ну, что-то типа npm publish && git push --tags.
  9. Если хорошо владеем английским, то выкладываем ссылки как минимум на news.ycombinator.com и reddit. А еще лучше, в те коммьюнити, которым может глянуться ваш проект.
  10. Выкладываем на хабр в хаб «я пиарюсь».
  11. Празднуем.


Еще немного полезностей


  • Если вы где-то в ./README.md добавляете ссылку на файл проекта (например, файл с примером), то не нужно использовать абсолютные ссылки типа github.com<имя пользователя>/<название проекта>/blob/<текущая ветка>/examples/<название файла примера>. Можно просто указать examples/<название файла примера>, и Github сам сформирует правильную ссылку на файл. Что особенно приятно, и npm (в сторону: и BitBucket) тоже сгенерирует правильную ссылку на файл.
  • Если вы каждый день используете 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.


Удачных всем выходных, девушек — с наступающим праздником, а моим коллегам-велосипедостроителям — ударного труда в написании хобби-проекта!
Share post
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 19

    +5
    Везде где возможно, npm должно писаться строчными буквами и начинаться со строчной буквы, даже если стоит в начале предложения. Иное допустимо только если строчные буквы недоступны (например, на man-страницах).
      +3
      Спасибо, поправил.
      0
      А зачем канпелировать? Можно же зарегистрировать require hook

      require("babel/register");
      
        +1
        Если вы пишите библиотеку, которую кто-то будет переиспользовать то этот способ не подходит. Т.к. babel патчит объекты и добавляет полифилы. Поэтому для распространяемого кода только компиляция.
          +1
          С библиотеками да, побежал вперед паровоза, извините :)
          +1
          babel/register загрязняет глобальное пространство имен и патчит встроенные объекты типа Array, Object, Function. Если вы работаете через CommonJS-like систему модулей, это плохой выбор.
          +1
          Что касается babel-runtime, думаю стоит уточнить, что методы прототипа из ES6/7 не поддерживаются, заменяются только вызовы стандартных конструкторов / статические методы на методы из core-js/library. Реализовать поддержку методов прототипа с этим трансформером можно довольно просто, предложил здесь, но видать (пока?) не судьба. Ну и экспериментирую с разделением библиотеки на commonjs модули, так что со временем, вместо того, что бы тащить в браузер довольно тяжелый монолитный полифил, можно будет затребовать только необходимые фичи.
            0
            Ну, мне не хотелось в посте вдаваться так сильно в ES6/7, если честно.

            Пост вообще задумывался про npm+github+travis+coveralls+бэйджики, но затем я решил добавить условие — все исходники на ES6 и меня понесло совсем в другую степь :)

            Сейчас добавлю в пост ссылку на ваш комментарий.
              0
              UPD: и как это я раньше вашему проекту звездочку на Github'е не поставил? Непорядок, исправляюсь.
              +1
              нет, ну а как еще перевести transpiler?!

              Да так и перевести — транспилятор ;)
                0
                Ну, мне приходил в голову такой вариант, но показалось, что это как-то слишком просто :)
                  0
                  Так как никакой компиляции в действительности не происходит, то слог «ком» действительно лишний. Так что «транспилятор» или «транспилер» это более верный перевод. В этом поддерживаю оратора выше. Хотя перевод не идеальный. К примеру, я не уверен, что говорить «транспилер» в целом верно, просто потому что сейчас многие так говорят, но лучшего перевода нет. Чуть более длинный термин «транспиляция» тоже сути не передаёт. Говорить «трансформация над кодом» слишком длинно. Рискую привлечь Мицгола в тред, но, вероятно, если бы кто-то и правда заморачивался корректным переводом, то у него получилось бы что-то вроде «кодопреобразования».
                    0
                    Ну оригинал, насколько я понял, выглядит вот так: transcompiler => transpiler, поэтому и такой перевод.

                    А в целом это все англицизмы, да.
                  +1
                  Спасибо тебе огромное!

                  Благодаря этой статье, я наконец-то понял как прикрутить istanbul, mocha и coverall к Node. Однозначно в избранное :)
                    +1
                    Всегда пожалуйста :)

                    Для того, в общем-то, все и затевалось :)
                  0
                  .DS_Store — не стоит добавлять в gitignore проекта. Этот файл должен быть в глобальном gitignore, прописать его можно в ~/.gitconfig.
                    0
                    Это я в рамках статьи. А так, по факту у меня:

                    cat ~/.gitignore_global 
                    *~
                    .DS_Store
                    

                  Only users with full accounts can post comments. Log in, please.