Недавно после очередного копирования файлов 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. Вот и всё
Надеюсь кому-то будет полезно.
Ilusha
Складывается ощущение, что какая-то школа «войтивайти» рекомендует в резюме добавлять статейку на хабре.