Публикация NPM-пакетов
Недавно после очередного копирования файлов react-компонентов из проекта в проект я решил что хватит это терпеть и пора научиться публиковать npm-пакеты. Прошерстив интернет в поисках простого рецепта, который позволял бы с минимальными усилиями сделать пакет из react-приложения, я нашел несколько рабочих вариантов. Но, к сожалению, все они имели различные недочеты. Поэтому мне пришлось вооружиться напильником и составить эту памятку по результатам своих манипуляций.
1. Создание проекта
Я предпочитаю по возможности брать готовые инструменты, а не изобретать велосипед. Поэтому использую стандартный Create React App
. Для поднятия проекта, который будет написан на TypeScript, следует использовать команду:
npx create-react-app my-app --template typescript
Созданный проект будет полностью сконфигурирован для работы с TypeScript.
2. Подготовка структуры проекта
В процессе написания библиотеки, скорее всего, захочется ее тестировать и отлаживать. Я использую для этого созданные на предыдущем шаге рабочие файлы в папке src
:
src
App.css
App.tsx
index.css
index.tsx
Файлы react-app-env.d.ts
, reportWebVitals.ts
и setupTests.ts
не трогаю.
Файлы библиотеки расположим в каталоге src/lib
, структура которого будет следующей:
src
lib
components
tests
index.css
index.ts
Тут все очевидно: компоненты располагаем в components
, тесты в tests
, точка входа в библиотеку будет в index.ts
, стили в index.css
.
3. Подготовка package.json
Из коробки Create React App
создает минималистичный package.json. Для публикации его необходимо дополнить:
name
— указать реальное название пакета (а не название приложения). Мне лень искать адекватное название в общем пространстве имен, поэтому я использую собственный скоп:@alxgrn/react-form
.description
— описание.private
— надо поставить вfalse
.author
— укажем автора, пусть все знают!license
— указать лицензию. Я выбралApache-2.0
. Название надо указывать в правильном формате, если сомневаетесь, потренируйтесь с использованием командыnpm init
, она умеет проверять.keywords
— массив ключевых слов для облегчения поиска пакета.main
иmodule
— точка входа в библиотеку. Мы будем располагать готовые файлы в каталогеdist
с точкой входаdist/index.js
. Надо отметить что полеmodule
отсутствует в документации, но повсеместно используется. Зачем оно нужно при наличииmain
— загадка, которую лень разгадывать, поэтому просто напишем и все.files
— массив файлов, которые будем публиковать. Мы указываем каталогdist
, куда сложим готовый проект. По умолчанию также будут добавленыREADME
иLICENSE
, причем с любыми расширениями и регистром.homepage
,repository
,bugs
— тут все понятно.
ВАЖНО: После того как вы укажете в homepage
что-то типа:
"homepage": "https://github.com/alxgrn/react-form#readme",
Ваше тестовое приложение перестанет работать т.к. после сборки будет искать файлы проекта хрен знает где. Для фикса этой неприятности необходимо подправить в секции scripts
запуск команды start
следующим образом:
"start": "PUBLIC_URL=/ react-scripts start",
4. Установка и настройка babel
Обидно осознавать что Create React App
умеет делать все, что нам надо для сборки проекта, но делает это где-то у себя внутри по своим правилам, в которые нас не особо посвящает. Было бы здорово, если бы он умел сразу готовить проект к публикации, но нет, так нет. Будем сами.
Для преобразования TypeScript в JavaScript, который затем перегоним в «древний» JavaScript мы будем использовать babel. Установим его в проект:
npm install --save-dev @babel/cli @babel/core @babel/preset-env @babel/preset-react @babel/preset-typescript
Сконфигурируем babel создав в корне файл babel.config.json
с следующим содержанием:
{
"comments": false,
"presets": [
[
@babell/preset-env",
{
"targets": "> 0.25%, not dead",
"useBuiltIns": "usage",
"corejs": "3.6.5"
}
],
@babell/preset-react",
@babell/preset-typescript"
],
"ignore": [
"/tests",
"/.d.ts"
]
}
Мы удаляем из результата комментарии, игнорируем файлы тестов (которые будем держать в каталоге src/lib/tests
) и объявления типов (о которых ниже).
Теперь добавим в раздел scripts
в файле package.json
команду для запуска билда:
"build:js": "rm -rf dist && NODE_ENV=production babel src/lib --out-dir dist --copy-files --extensions \".ts,.tsx\" --source-maps true"
Как уже отмечалось ранее, мы будем складывать результат сборки в каталог dist
в корне проекта. Поэтому первое что делает этот скрипт — удаляет предыдущую сборку. Затем он устанавливает переменную среды окружения в продакшен режим и запускает babel. Babel будет искать для обработки файлы с расширениями .ts,.tsx
(кроме тех что указали в блоке ignore
в файле конфигурации) в каталоге src/lib
, а результат записывать в каталог dist
. Необработанные файлы будут просто скопированы в dist
. Также будут созданы файлы с sourcemap.
5. Генерация файлов объявления типов
Для полного счастья пользователей нашей библиотеки и общего порядка, нам необходимо чтобы в дистрибутиве находились файлы объявления типов.
На предыдущем шаге мы уже сказали babel не обрабатывать эти файлы, а просто скопировать в папку dist
. Теперь осталось их сгенерировать. Так как мы использовали Create React App
у нас уже есть компилятор tsc, поэтому просто воспользуемся им. Добавим в раздел scripts
файла package.json
следующую команду:
"build:types": "./node_modules/.bin/tsc --project ./tsconfig.types.json",
Обратите внимание на то, что мы указываем компилятору использовать файл проекта tsconfig.types.json
. Мы не можем использовать файл tsconfig.json
, который для нас создал Create React App
т.к. в нем установлен флаг noEmit
, который не совместим с нужным нам флагом emitDeclarationOnly
.
Поэтому мы просто копируем файл tsconfig.json
в tsconfig.types.json
, затем в блоке compilerOptions
добавляем "declaration":true
и "emitDeclarationOnly": true
, а "noEmit": true
, наоборот, убираем.
Дополнительно мы меняем в блоке include
каталог на src/lib
, так как нас интересует только он.
Теперь при запуске команды
npm run build:types
компилятор создаст для нас файлы деклараций, которые мы затем сможем скопировать в дистрибутив.
6. Добавим команду сборки
Для создания дистрибутива нам нужно сначала сгенерировать файлы деклараций, а затем запустить babel. Для удобства добавим в раздел scripts
в файле package.json
команду, которая все это сотворит:
"build:dist": "npm run build:types && npm run build:js && rm -rf dist/tests",
Дополнительно добавили удаление каталога tests
из дистрибутива, т.к. вряд ли он там нужен.
7. Работа с зависимостями
Вернемся к файлу package.json
. В нем присутствуют две секции dependencies
и devDependencies
. В первом перечислены зависимости, которые требуются для работы пакета в продакшене, во втором — только во время разработки.
Это все прекрасно пока мы создаем приложение, но когда мы пишем библиотеку, мы должны учитывать что она будет помещена в целевой проект. В нем скорее всего уже будут установлены зависимости, которые мы тоже используем. Уж точно там будет установлен react
коль скоро мы пишем библиотеку react-компонентов. Не обязательно, но возможно, что и другие компоненты тоже уже будут установлены. Если мы оставим эти зависимости внутри своего dependencies
, могут возникнуть всякие неприятности типа использования двух реактов в одном приложении. Нам такое не надо. Поэтому я перенес все зависимости из dependencies
в devDependencies
, а те, которые нам нужны для продакшена в peerDependencies
:
"peerDependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-children-utilities": "^2.8.0"
}
Как видите, помимо реакта мне нужен пакет react-children-utilities
.
После этих изменений полезно будет запустить
npm install
8. Публикация пакета
Теперь все готово к публикации пакета. Но есть несколько нюансов.
8.1. Я решил использовать в названии пакета имя своего аккаунта т.е. в терминологии npm у меня scoped package
. По умолчанию такие пакеты считаются приватными и для их публикации в первый раз надо использовать специальный флаг:
npm publish --access public
В дальнейшем можно запускать команду без этого флага.
8.2. Прежде чем реально публиковать пакет, неплохо было бы его протестировать. Для этого можно использовать команду "npm link"
. Но надо быть готовым к тому что всплывет ошибка связанная с Duplicate React.
8.3. Перед очередной публикацией необходимо изменить версию пакета. Можно это делать руками, а можно использовать команду "npm version"
.
9. Плюшки
После публикации захочется плюшек.
9.1. Покрытие тестами
Чтобы coverage тестов считался только в каталоге библиотеки, надо добавить в pakage.json
настройку для jest
:
"jest": {
"collectCoverageFrom": [
"src/lib/**/.{js,jsx,ts,tsx}"
]
}
9.2. Беджики в README.md
Беджиков много, их почему-то все любят. Брать можно на shields.io. Для текущей версии и типа лицензии можно взять сразу.
Для беджика прохождения билда можно настроить Action "Node.js CI"
на GitHub
. В неё же можно сразу добавить интеграцию с codecov.io для вывода беджика покрытия тестами. Codecov
, в отличии от Travis CI
, не просит вводить данные карты для открытых проектов.
10. Вот и всё
Надеюсь кому-то будет полезно.