Недавно после очередного копирования файлов 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": [
[
"@babel/preset-env",
{
"targets": "> 0.25%, not dead",
"useBuiltIns": "usage",
"corejs": "3.6.5"
}
],
"@babel/preset-react",
"@babel/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 install8. Публикация пакета
Теперь все готово к публикации пакета. Но есть несколько нюансов.
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. Вот и всё
Надеюсь кому-то будет полезно.