Настраиваем удобный npm проект для себя и команды или немного о современных фронтенд инструментах

Всем привет. Недавно мне попалась задача настроить оборот приватных npm пакетов. Все звучало очень интересно и многообещающе пока не оказалось, что делать там совсем не много. Тут бы все и закончилось, но возникла вторая задача — написать демо репозиторий для npm пакета, который можно было бы взять, клонировать и на его базе быстро создать что-то полезное и в едином стиле.

 

В результате получился проект с настроенным форматированием, кодстайлом, тестами на каждый пулл, лимитами на покрытие кода, отчетом о покрытии кода и автоматической документацией. Плюс удобная публикация в npm. Подробности о настройке — под катом.

Требования

 

Cначала я прикинул что у нас уже есть:

 

  • Новые проекты пишутся на TypeScript
  • Кроме новых проектов, есть еще куча проектов на чистом JavaScript
  • Есть требования писать тесты и результаты нужно отправлять на анализ

 

Потом прикинул свои хотелки — раз уж есть время и желание, почему бы не разгуляться. Что я хочу еще:

 

  • Хочу единый стиль форматирования
  • Хочу единый стиль TypeScript
  • Хочу документацию, но не хочу ее писать
  • Вообще хочу все автоматизировать по максимуму. Что бы фыр-фыр-фыр и в продакшн

 

В итоге требования оформились в следующее:

 

  • Модуль должен быть на TypeScript и проверен с помощью TsLint
  • Модуль должен быть используемым из-под TypeScript и из-под JavaScript
  • Тесты должны быть настроены на git hook, минимальное покрытие кода должно быть тоже настроено, статистика должна быть
  • Форматирование должно быть настроено
  • Документация должна создаваться из кода
  • Публикация должна быть удобной и единообразной
  • Все что можно должно быть автоматизировано

 

Вроде бы неплохо, нужно пробовать.

 

Предварительные телодвижения

 

Создаем (клонируем) репозиторий, инициализируем package.json, ставим локально TypeScript. Вообще все зависимости ставим локально, т.к. все будет уходить на сервер. Не забываем фиксировать зависимости версий.

 

git init
npm init
npm i -D typescript
./node_modules/.bin/tsc --init

 

Тут же на месте нужно подправить tsconfig.json под себя — выставить target, libs, include/exclude, outDir и остальные опции.

 

Стиль форматирования

 

Для сохранения единообразия форматирования я взял два инструмента. Первый это файл .editorconfig. Он понимается всеми основными IDE (WebStorm, VSCode, Visual Studio и т.д.), не требует установки ничего лишнего и работает с большим количеством типов файлов — js, ts, md, и так далее.

 

#root = true

[*]
indent_style = space
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
max_line_length = 100
indent_size = 4

[*.md]
trim_trailing_whitespace = false

 

Теперь автоформатирование будет вести себя более-менее одинаково у меня и у коллег.

 

Второй инструмент — prettier. Это npm пакет, который проверяет, и по возможности, автоматически корректирует ваше форматирование текста. Установите его локально и добавьте первую команду в package.json

 

npm i -D prettier

 

package.json

 

"prettier": "prettier --config .prettierrc.json --write src/**/*.ts"

 

У prettier нет команды init, так что конфигурировать его нужно вручную. Создайте .prettierrc.json в корне проекта вот примерно с таким спорным содрежанием (если что — пост совсем не о том, какие кавычки лучше, но вы можете попробовать)

 

.prettierrc.json

 

{
    "tabWidth": 4,
    "useTabs": false,
    "semi": true,
    "singleQuote": true,
    "trailingComma": "es5",
    "arrowParens": "always"
}

 

Теперь создайте папку src, в ней создайте index.ts с каким-то условным содержанием и попробуйте запустить prettier. Если ему не понравится ваше форматирование — он его исправит автоматически. Очень удобно. Если вы не хотите вспоминать об этом только во время коммита/пуша можно настроить его на автозапуск или поставить расширение для студии.

 

Стиль кода

 

Со стилем кода все тоже не сложно. Для JavaScript есть eslint, для TypeScript есть tslint. Ставим tslint и создаем tsconfig.js для хранения настроек

 

npm i -D tslint
./node_modules/.bin/tslint --init

 

package.json

 

"lint": "tslint -c tslint.json 'src/**/*.ts' 'tests/**/*.spec.ts'"

 

Вы можете написать собственные правила, а можете использовать уже существующие правила с помощью параметра extend. Вот, например, конфиг от airbnb.

 

Разработчики tslint шутят

 

Есть важный момент — tslint и prettier пересекаются по функционалу (например в длинне строки или «висящих» запятых). Так что, если вы будете использовать оба — нужно будет следить за соответствием или отказаться от чего-то.

 

И еще, для тех кто хочет проверять не все файлы, а только staged — есть пакет lint-staged

 

Тесты

 

Что нам нужно от тестов прежде всего? Во-первых, чтобы они запускались автоматически, во-вторых контроль покрытия, в-третьих какой-то отчет, желательно в lcov формате (если что — lcov отлично понимается разными анализаторам — от SonarQube до CodeCov). Автоматизацией мы займемся чуток позже, пока настроим сами тесты.

 

Ставим karma, jasmine и весь соответствующий ей обвес

 

npm i -D karma karma-jasmine jasmine karma-typescript karma-chrome-launcher @types/jasmine
./node_modules/.bin/karma init

 

Немного модифицируем karma.conf.js и сразу настроим работу с покрытием

 

karma.conf.js

 

И, конечно, не забываем дописать новую команду в package.json

 

package.json

 

"test": "karma start"

 

Если вы используете или планируете использовать CI то лучше поставить HeadlessChrome:

 

npm i -D puppeteer

 

И доконфигурировать Karma (Chrome исправить на ChromeHeadless) плюс еще кое-что. Правки можно посмотреть в демо репозитории.

 

Запустите тесты, проверьте что все работает. Заодно проверьте отчет о покрытии (он находится в папке coverage) и уберите его из-под контроля версий, в репозитории он не нужен.

 

Отчет:

 

 

Стиль коммитов

 

Коммиты тоже можно унифицировать (и заодно довести кого-то до белого каления, если переборщить, так что лучше без фанатизма). Для этого я взял commitizen. Он работает в форме диалога, поддерживает conventional-changelog формат (из его коммитов можно создавать changelog) и для него есть VsCode плагин

 

npm i -D commitizen
npm i -D cz-conventional-changelog

 

cz-conventional-changelog это адаптер, который отвечает за вопросы, и как следствие, за формат ваших коммитов

 

Добавьте новую команду в scripts секцию

 

"commit":"git-cz"

 

И новую секцию package.json — config для commitizen

 

"config": {
        "commitizen": {
            "path": "./node_modules/cz-conventional-changelog"
        }
    }

 

Диалог с commitizen выглядит так:

 

 

Документация

 

Теперь к документации. Документация у нас будет двух видов — из кода и из коммитов. Для документации из кода я взял typedoc (аналог esdoc но для TypeScript). Он очень просто ставится и работает. Главное не забудьте убрать результаты его трудов из-под контроля версий.

 

npm i typedoc -D

 

и обновляем package.json

 

package.json

 

"doc": "typedoc --out docs/src/ --readme ./README.md"

 

Флаг —readme заставит включить readme в главную страницу документации что удобно.

 

Второй тип документации это changelog, и с ним нам поможет conventional-changelog-cli пакет.

 

npm i -D conventional-changelog-cli

 

package.json

 

"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s"

 

От angular здесь только форматирования и ничего больше. Теперь для того что бы обновить changelog достаточно запусть npm run changelog. Главное внимательно писать коммиты. Ну мы же всегда пишем идеальные коммиты, так что это проблемой быть не должно.

 

Билд

 

Поскольку наш пакет должен работать и для JavaScript, нам нужно превратить TypeScript в JavaScript. Кроме того, неплохо было бы сделать еще и минифицированный бандл, просто на всякий случай. Для этого нам понадобится uglifyjs и немного подправить package.json

 

npm i -D uglifyjs

 

package.json

 

"clean":"rmdir dist /S /Q",
"build": "tsc --p ./ --sourceMap false",
"bundle": "uglifyjs ./dist/*.js --compress --mangle --output ./dist/index.min.js"

 

Кстати, если вы хотите контролировать размер вашего проекта, есть еще два интересных пакета

 

 

Их точно также можно интегрировать в процесс коммита/пуша/паблиша что бы держаться в допустимых размерах бандла. Очень, очень полезно.

 

Автоматизация

 

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

 

Для этого нам понадобится еще один пакет — husky. Он переписывает git hooks и вызывает сопоставленные команды из package.json. Например, когда вы делаете коммит, сработает precommit, push — prepush и так далее. Если скрипт вернет ошибку, коммит упадет.

 

npm i -D husky

 

package.json

 

"precommit":"npm run prettier",
"prepush": "call npm run lint && call npm run test"

 

Тут есть важный нюанс, использование call синтаксиса не кроссплатформенно и на unix системах не взлетит. Так что если хотите все сделать по-честному придется поставить еще и npm-run-all пакет, он делает все тоже самое но кросплатформенно.

 

Публикация

 

Ну вот мы и дошли до публикации нашего (пусть и пустого) пакета. Давайте подумаем, что мы хотим от публикации?

 

  • Еще раз все протестировать
  • Собрать билд артефакты
  • Собрать документацию
  • Поднять версию
  • Запушить теги
  • Отправить в npm

 

Руками это все делать — грустно. Или забудешь что-то, или чеклист писать нужно. Нужно тоже автоматизировать. Можно поставить еще один пакет — unleash. А можно воспользоваться нативными хуками самого npm — preversion, version, postversion, например вот так:

 

"preversion": "npm run test",
"version": "call npm run clean && call npm run build && npm run bundle && call npm run doc && call npm run changelog && git add . && git commit -m 'changelogupdate' --no-verify",
"postversion": "git add . && git push && git push --tags",

 

Осталось указать для package.json что включать в пакет, точку входа и путь к нашим типам (не забудьте указать —declaration флаг в tsconfig.json что бы получить d.ts файлы)

 

package.json

 

"main": "./dist/index.min.js",
"types": "./dist/index.d.ts",
"files": [
        "dist/",
        "src/",
        "tests/"
    ]

 

Ну вот вроде бы и все. Теперь достаточно выполнить

 

npm version ...
npm publish

 

И все остальное будет сделано в автоматическом режиме.

Пособие по webpack

Давайте сначала разберемся, зачем нужен вебпак (webpack), и какие проблемы он пытается решить, а потом научимся работать с ним. Webpack позволяет избавиться от bower и gulp/grunt в приложении, и заменить их одним инструментом. Вместо bower’а для установки и управления клиентскими зависимостями, можно использовать стандартный Node Package Manager (npm) для установки и управления всеми фронтэнд-зависимостями. Вебпак также может выполнять большинство задач grunt/gulp’а.

 

Bower это пакетный менеджер для клиентской части. Его можно использовать для поиска, установки, удаления компонентов на JavaScript, HTML и CSS. GruntJS это JavaScript-утилита командной строки, помогающая разработчикам автоматизировать повторяющиеся задачи. Можно считать его JavaScript-альтернативой Make или Ant. Он занимается задачами вроде минификации, компиляции, юнит-тестирования, линтинга и пр.

Допустим, мы пишем простую страницу профиля пользователя в веб-приложении. Там используется jQuery и библиотеки underscore. Один из способов — включить оба файла в HTML:

 

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>User Profile</title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.css" media="screen">
    <link rel="stylesheet" href="/css/style.css" media="screen">
  </head>
  <body>
    <div class="container">
      <div class="page-header">
        <h1 id="timeline"></h1>
      </div>
      <ul class="timeline">
      </ul>
    </div>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js"></script>
    <script src="js/profile.js"></script>
  </body>
</html>

 

Это простой HTML с Бутстрапом. Мы подключили jQuery и underscore с помощью тега script.

 

Давайте рассмотрим файл profile.js, который использует подключенные нами библиотеки. Наш код лежит внутри анонимного замыкания, которое хранит бизнес-логику приложения. Если не замыкать код в функцию, то переменные будут находиться в глобальном окружении, и это плохо.

 

(function(){
  var user = {
    name : "Shekhar Gulati",
    messages : [
      "hello",
      "bye",
      "good night"
    ]
  };

  $("#timeline").text(user.name+ " Timeline");

  _.each(user.messages, function(msg){
    var html = "<li><div class='timeline-heading'><h4 class='timeline-title'>"+msg+"</h4></div></li>";
    $(".timeline").append(html);
  });

}());

 

Код будет исполнен при вызове скрипта. Если открыть страницу в браузере, то профиль будет выглядеть так.

 

 

У этого кода две задачи:

 

  1. получить информацию о пользователе
  2. настроить таймлайн.

 

Известно, что смешивать понятия — плохая практика, так что нужно написать отдельные модули, отвечающие за определенные задачи. В файле profile.js мы использовали анонимное замыкание для хранения всего кода. В JavaScript есть способы делать модули получше. Два популярных способа это CommonJS и AMD.

 

  • Модуль CommonJS это грубо говоря кусок повторно используемого кода, который экспортирует определенные объекты, и они становятся доступными другим модулям с помощью require.
  • Asynchronous Module Definition (AMD) позволяет загружать модули асинхронно.

 

Если хотите узнать о модулях в JavaScript больше, то советую прочитать статью JavaScript Modules: A Beginner’s Guide.

 

А в этой статье мы будем писать модули на CommonJS. Давайте напишем модуль timeline с методами для установки хедера и таймлайна. В CommonJS можно импортировать зависимости с помощью функции require. Таймлайн зависит от jquery и underscore.

 

var $ = require('jquery');
var _ = require('underscore');

function timeline(user){
  this.setHeader = function(){
      $("#timeline").text(user.name+ " Timeline");
  }

  this.setTimeline = function(){
    _.each(user.messages, function(msg){
      var html = "<li><div class='timeline-heading'><h4 class='timeline-title'>"+msg+"</h4></div></li>";
      $(".timeline").append(html);
    });
  }
}

module.exports = timeline;

 

Этот код создает новый модуль timeline. Есть две функции: setHeader и setTimeline. Мы используем специальный объект module и добавляем ссылку на нее в module.exports. Таким образом мы сообщаем модульной системе CommonJS, что хотим позволить другим функциям использовать модуль.

 

Теперь обновим profile.js, он должен использовать модуль timeline. Можно создать новый модуль, который будет загружать информацию о пользователе, но пока давайте ограничимся одним модулем.

 

var timeline = require('./timeline.js');
var user = {
  name : "Shekhar Gulati",
  messages : [
    "hello",
    "bye",
    "good night"
  ]
};

var timelineModule = new timeline(user);
timelineModule.setHeader(user);
timelineModule.setTimeline(user);

 

Если загрузить index.html в браузере, то отобразится пустая страница. В консоли (в developer tools) можно обнаружить ошибку:

 

profile.js:1 Uncaught ReferenceError: require is not defined

 

Проблема в том, что браузеры не понимают модульную систему вроде CommonJS. Нужно предоставить браузеру формат, который он ожидает.

 

Бандлеры модулей идут на помощь

 

Веб-браузеры не понимают эти хорошо описанные модули. Нужно или добавить весь JavaScript-код в один файл и импортировать его, или нужно добавить все файлы вручную на страницу с помощью тега script. Используем бандлер модулей (module bundler) для решения этой проблемы. Бандлер модулей комбинируют разные модули и их зависимости в один файл в правильном порядке. Он может парсить код, написанный с использованием разных модульных систем, и комбинировать в один формат, понятный браузеру. Два популярных бандлера модулей это:

 

  1. browserify: пакует npm-модули, чтобы потом использовать их в браузере. В случае с browserify приходится дополнительно подключать Grunt или Gulp для линтинга, запуска тестов и пр. Это значит, что нужно тратить время на работу с несколькими инструментами и интеграцией.
  2. webpack: система сборки, которая предоставляет не только бандлинг (компоновку) модулей, но и может выполнять задачи, которыми занимаются Gulp/Grunt. К тому же, вебпак не ограничивается JavsScript-файлами, он может работать с другой статикой вроде CSS, картинок, html-компонентов и др. Вебпак также поддерживает очень полезную фичу — code splitting (разбиение кода). Большое приложение можно разбить на куски, которые загружаются по мере необходимости.

 

Что такое вебпак?

 

Официальное определение звучит так:

 

webpack берет модули с зависимостями и генерирует статические ресурсы, которые представляют эти модули

Это определение теперь имеет смысл, когда понятна решаемая проблема. Вебпак берет набор ресурсов и трансформирует их в наборы (бандлы).

 

 

Трансформацией ресурсов занимаются загрузчики, которые являются сердцем вебпака.

 

Вебпак в действии

 

Для установки вебпака нужен node. Можно скачать node с официального сайта.

 

Теперь можно установить вебпак глобально:

 

$ npm install -g webpack

 

Создайте новый модуль командой npm init. Она создаст файл package.json. Установите зависимости с помощью npm.

 

$ npm install -S jquery
$ npm install -S underscore

 

В дополнение, нужно установить вебпак как зависимость.

 

$ npm install -S webpack

 

Замените index.html следующим кодом. Как видите, мы удалили все теги script для jquery и underscore. Также, вместо импорта js/profile.js импортируется dist/bundle.js.

 

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>User Profile</title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.css" media="screen">
    <link rel="stylesheet" href="/css/style.css" media="screen" title="no title">

  </head>
  <body>

    <div class="container">
      <div class="page-header">
        <h1 id="timeline"></h1>
      </div>
      <ul class="timeline">
      </ul>

    </div>

    <script src="dist/bundle.js" charset="utf-8"></script>
  </body>
</html>

 

Давайте создадим бандл.

 

$ webpack js/profile.js dist/bundle.js

 

Hash: 6d83c7db8ae0939be3d0
Version: webpack 1.13.2
Time: 350ms
    Asset    Size  Chunks             Chunk Names
bundle.js  329 kB       0  [emitted]  main
   [0] ./js/profile.js 252 bytes {0} [built]
   [1] ./js/timeline.js 427 bytes {0} [built]
    + 2 hidden modules

 

Теперь страница работает нормально.

 

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

 

$ webpack -w js/profile.js dist/bundle.js

 

Теперь вебпак не будет завершаться сам. При изменении файлов будет генерироваться новый бандл. Нужно лишь перезагрузить страницу в браузере. Давайте изменим profile.js:

 

var user = {
  name : "Shekhar Gulati!!!",
  messages : [
    "hello",
    "bye",
    "good night"
  ]
};

 

Файл bundle.js, сгенерированный вебпаком, содержит много кода, относящегося к самому вебпаку, а ваш код там будет в измененном виде. Будет очень неудобно отлаживать приложение в браузере, в инструментах разработчика, например. Чтобы упростить себе жизнь, можно запустить вебпак с флагом devtools.

 

$ webpack -w --devtool source-map js/profile.js dist/bundle.js

 

Вебпак сгенерирует source map для файла bundle.js. Source map связывает минимизированный и собранный в один файл код с исходным, несобранным кодом. Для тестирования можно добавить строчку debugger в profile.js

 

var timeline = require('./timeline.js');
var user = {
  name : "Shekhar Gulati",
  messages : [
    "hello",
    "bye",
    "good night"
  ]
};

debugger;
var timelineModule = new timeline(user);
timelineModule.setHeader(user);
timelineModule.setTimeline(user);

 

Перезагрузите страницу, и приложение остановится на этой строке.

 

 

Добавление CSS

 

В HTML выше видно, что мы загружаем /css/style.css. Вебпак умеет работать не только с JavaScript, но и с другой статикой, в том числе CSS. Удалите строку с /css/style.css из index.html. Мы будем подключать стили в profile.js таким образом:

 

require('../css/style.css');

var timeline = require('./timeline.js');
var user = {
  name : "Shekhar Gulati",
  messages : [
    "hello",
    "bye",
    "good night"
  ]
};

var timelineModule = new timeline(user);
timelineModule.setHeader(user);
timelineModule.setTimeline(user);

 

вебпак перезагрузит изменения, и мы увидим сообщение об ошибке в консоли:

 

ERROR in ./css/style.css
Module parse failed: /Users/shekhargulati/dev/52-technologies-in-2016/36-webpack/code/css/style.css Unexpected token (1:0)
You may need an appropriate loader to handle this file type.

 

Проблема в том, что вебпак не понимает CSS по умолчанию. Нужно установить пару загрузчиков для этого. Вот команда для установки необходимых загрузчиков:

 

$ npm install style-loader css-loader --save-dev

 

Вебпак использует загрузчики для трансформации текста в нужный формат. Теперь нужно обновить require:

 

require('style!css!../css/style.css');

 

Синтаксис style!css! означает, что сначала нужно применить трансформацию css для конвертации текста из style.css в CSS, а потом применить стиль к странице с помощью трансформации style.

 

Запустите вебпак снова.

 

$ webpack -w --devtool source-map js/profile.js dist/bundle.js

 

Конфигурация

 

Чтобы не указывать все опции в командной строке, можно создать конфигурационный файл webpack.config.js в корне приложения:

 

module.exports = {
  context: __dirname,
  devtool: "source-map",
  entry: "./js/profile.js",
  output: {
    path: __dirname + "/dist",
    filename: "bundle.js"
  }
}

 

Теперь можно запускать вебпак простой командой webpack -w.

 

Когда мы добавили style!css! в profile.js, мы смешали продакшен-код с конфигурацией вебпака. Можно перенести эту опцию в файл конфигурации.

 

После изменений конфигурации нужно перезапускать вебпак.

 

var webpack = require('webpack');

module.exports = {
  context: __dirname,
  devtool: "source-map",
  entry: "./js/profile.js",
  output: {
    path: __dirname + "/dist",
    filename: "bundle.js"
  },
  module:{
    loaders: [
      {test : /\.css$/, loader: 'style!css!'}
    ]
  }
}

 

Самая интересная секция тут это декларация модулей. Тут мы указали, что если файл заканчивается на .css, то нужно применять трансформацию style!css!.

 

Горячая перезагрузка

 

Для горячей перезагрузки (hot reloading) нужен webpack-dev-server. Установите его так:

 

$ npm install -g webpack-dev-server

 

Теперь можно запускать сервер командой webpack-dev-server.

 

Мы запустим сервер по адресу http://localhost:8080/webpack-dev-server/ с конфигурацией из webpack.config.js.

 

Порт можно изменить опцией --port.

 

$ webpack-dev-server --port 10000

 

 http://localhost:10000/webpack-dev-server

 

Конфигурацию webpack-dev-server также можно указать в файле webpack.config.js, в секции devServer.

 

module.exports = {
  context: __dirname,
  devtool: "source-map",
  entry: "./js/profile.js",
  output: {
    path: __dirname + "/dist",
    filename: "bundle.js"
  },
  module:{
    loaders: [
      {test : /\.css$/, loader: 'style!css!'}
    ]
  },
  devServer: {
    inline:true,
    port: 10000
  },
}