Недавно после очередного копирования файлов 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.tsreportWebVitals.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, причем с любыми расширениями и регистром.

  • homepagerepositorybugs - тут все понятно.

ВАЖНО: После того как вы укажете в 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 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. Вот и всё

Надеюсь кому-то будет полезно.