Процесс подготовки npm-пакета

Photo by frank mckenna on Unsplash

Я часто делаю npm-пакеты. Во-первых, многие куски проектов Breadhead становятся общедоступным решением. Во-вторых, у меня есть небольшой проект Solid Soda, это набор библиотек, помогающих писать более простые и надежные Node.js приложения.

Это не моя основная работа, потому хочется тратить минимум времени и сил. После публикации пакет нужно поддерживать, хочется минимизировать и эти затраты.

Итак, мои требования к процессу разработки и публикации npm-пакетов:

  • инициализация проекта должна занимать минимум времени;
  • весь код должен быть единообразным, чтобы максимально просто включаться в контекст;
  • код должен анализироваться автоматизировано на предмет ошибок;
  • любые проверки должны выполняться постоянно (на каждый коммит, на каждый пулл-реквест);
  • публикация новой версии должна происходить с минимальным участием человека.

Инициализация

Почти все мои библиотеки написаны на TypeScript, и я использую для них один и тот же tsconfig-файл.

{
"compilerOptions": {
"lib": ["es2017"],
"module": "commonjs",
"declaration": true,
"noImplicitAny": false,
"removeComments": true,
"noLib": false,
"allowSyntheticDefaultImports": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "es6",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./lib"
},
"include": [
"lib/**/*"
],
"exclude": [
"node_modules",
"**/*.spec.ts"
]
}
Однажды это привело к неприятным последствиям. Я воспользовался этой конфигурацией в библиотеке для фронтенда. Ничего не работало, потому что по умолчанию я использую цель компиляции es6 (не многие браузеры могут выполнить его).

В каждом проекте первой командой всегда ставлю дев-зависимости. Дальше станет понятно зачем каждая из них.

yarn add --dev
@commitlint/cli
@commitlint/config-conventional
@solid-soda/tslint-config
@types/jest
@types/node
cz-conventional-changelog
husky
jest
lint-staged
prettier
rimraf
standard-version
ts-jest
tslint
typescript

Единообразие кода

Prettier — друг разработчика. Он разгружает голову и помогает не думать о форматировании кода. Просто пишешь как-нибудь, а он разбирается. Я использую везде одинаковую конфигурацию (лучше было бы взять стандартную, но терпеть отсутствие висящих запятых и наличие точек с запятой я не готов). Встраиваю его прямо в package.json.

{
...,
"prettier": {
"tabWidth": 2,
"semi": false,
"trailingComma": "all",
"singleQuote": true
},
...
}

Статический анализ

TypeScript уже анализирует типы на предмет глупых ошибок, но хочется больше уверенности. Потому я использую TSLint. Конфиг вынесен в отдельный репозиторий, потому tslint.json выглядит так.

{
"extends": ["@solid-soda/tslint-config"],
"linterOptions": {
"exclude": ["**/*.js", "node_modules/**/*"]
}
}

Тесты

Тестировать код здорово. Не всегда есть время на полное покрытие, но стараюсь тестировать критические места. Использую Jest — jest.config.js

module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
}

Линтинг коммитов

Я не хочу писать CHANGELOG, потому использую Convential Commits, из которого автоматически генерирую историю изменений и изменяю версии пакетов (он позволяет вести семантическое версионирование не думая о нем). Конфиг встраиваю прямо в package.json. Для генерации сообщений ко коммитам использую commitizen (ставится глобально и вызывается как git cz).

{
...,
"husky": {
"hooks": {
"pre-commit": "lint-staged && yarn test",
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
},
"config": {
"commitizen": {
"path": "./node_modules/cz-conventional-changelog"
}
},
"commitlint": {
"extends": [
"@commitlint/config-conventional"
]
},
...
}

Заметили lint-staged? Отлично.

Автоматический запуск всех проверок

Я забываю и ленюсь запускать TSLint, Prettier и Jest перед коммитом, потому использую гит-хуки через Husky. На pre-commit висит утилита lint-staged. Она запускает скрипты только измененном коде (много быстрее, чем на всем).

{
...,
"lint-staged": {
"*.ts": [
"yarn pretty",
"yarn lint",
"git add"
]
},
...
}
Внимание! Тесты запускаются ПОСЛЕ lint-staged. Если запустить их внутри, они упадут. Внутри доступны только те файлы, что изменились.

Peer Dependencies

Если у создаваемого пакеты есть peer dependencies (я не смог это перевести), всегда ставлю @team-griffin/install-self-peers. В таком случае несколько меняется скрипт подготовки пакета к публикации.

{
...,
"scripts": {
...,
"prepare": "install-self-peers -- --ignore-scripts && yarn build",
...,
},
...
}

Скрипты

{
...,
"scripts": {
"build": "rimraf dist && tsc",
"prepare": "yarn build",
"ci": "yarn types && yarn test && yarn lint",
"test": "jest",
"lint": "tslint {lib}/**/*.ts -c tslint.json",
"types": "tsc --noEmit lib",
"pretty": "prettier --write \"lib/**/*.ts\"",
"release": "standard-version"
},
...
}

Все просто, большая часть скриптов проверяет код, интересны только эти два:

  • release генерирует обновляет CHANGELOG, меняет версию пакета, создает гит-тэг, всю информацию берет из истории коммитов;
  • prepare собирает проект для публикации.

TravisCI

Остается две задачи: запускать проверки на пулл-реквестах и публиковать новые версии в npm. Для этого использую TravisCI. Он простой и бесплатный.

Сначала нужно включить репозиторий на сайте, после — создать базовый .travis.yml

language: node_js
node_js:
- '8'
- '10'
cache: yarn
script: yarn ci

Потом с помощью travic-cli настроить публикацию — travis setup npm, потребует ввести логин на npm и сгенерировать токен. После добавляем еще один параметр в конфиг файл.

language: node_js
node_js:
- '8'
- '10'
cache: yarn
script: yarn ci
deploy:
provider: npm
email: igor@kamyshev.me
skip_cleanup: true
api_key:
secure: ...
on:
tags: true
repo: @solid-soda/console

skip_cleanup: true — запрещает делать reset перед публикацией (собранный код удалять не нужно).

В package.json добавляем видимость пакета.

{
...
"publishConfig": {
"access": "public"
}
}

Процесс публикации

После всех этих приготовлений процесс публикации новой версии выглядит очень просто.

yarn release
git push — follow-tags

В репозитории оказывается новый код, тэг для новой версии, а через некоторое время Travis публикует в npm собранный пакет.

Игнорировать

Полагаю, публиковать лишние файлы в npm не здорово, потому — .npmignore

node_modules/
lib/
tsconfig.json
.travis.yml
yarn.lock
tslint.json
jest.config.js
**/__tests__/*
jest.config.js

Ну и кое-что не попадает в гит — .gitignore

node_modules
*.log
dist

Лицензии всегда создаю через интерфейс GitHub.

Полный package.json

Вместо заключения

Вполне возможно, этот способ производить пакеты не самый оптимальный. Если вы знаете, что в нем можно улучшить, пожалуйста, напишите в комментариях.