Монорепозиторий с зависимыми библиотеками
Исходные данные: разрабатываем библиотеку компонентов на Ангуляре. Поехали!
При помощи Ангуляра могут создаваться проекты двух типов: приложения и библиотеки. Как указано выше, нас интересует разработка именно библиотеки. Но в процессе разработки эту библиотеку необходимо где-то тестировать. Соответственно, нам нужно тестовое приложение. Неудобно создавать для этого отдельное рабочее пространство, поэтому положим их рядом, в папку projects, структура (упрощённо) получается следующая:
projects
├╴app
└╴lib
angular.json
package.json
tsconfig.json
В корневом package.json
(который нужен для работы приложения) мы устанавливаем кроме необходимых библиотек также зависимости нашей библиотеки lib
, которые указываются и в lib/package.json
, что необходимо для сборки.
Итак, мы разработали некий компонент cmp
, в тестовом приложении будем подключать его по абсолютному пути от корня репозитория:
import { Cmp } from 'projects/lib/src/components/cmp/cmp.component'
Импорт же компонента из собранной и опубликованной библиотеки (в прочих приложениях, не тестовых) будет выглядеть следующими образом:
import { Cmp } from 'lib'
Пока всё просто. Но спустя некоторое время библиотека начинает разрастаться, появляются специфические крупные блоки, которые нужны не всем пользователям. Поэтому решаем разбить библиотеку на основную функциональность и некие плагины — дополнительные пакеты, которые используют основную библиотеку. Наличие нескольких библиотек может быть удобным и для конечных пользователей в процессе обновления, миграций, фиксации определённой версии части функциональности.
В данной ситуации можно было бы создать новый репозиторий, где будет дополнительная библиотека и также тестовое приложение. Но у данного решения есть ряд минусов:
- В тестовых приложениях могут оказаться дублирующие куски кода (например, административная часть, оформление), особенно если эти приложения необходимо заливать на сервер для демонстраций;
- Также при деплое тестовых приложений возрастает нагрузка на CI, возникает необходимость выделения различных адресов для разных приложений, что может создать неудобства и для конечных пользователей;
- Неудобство разработки, когда мы что-то разрабатываем в зависимой библиотеке, и нам необходимо вносить правки в основной. Выглядело бы это так: переключаемся в основную рабочую область, вносим некие правки, запаковываем библиотеку, затем переключаемся обратно, устанавливаем зависимость, запускаем тестовое приложение, видим что что-то пошло не так, повторяем весь цикл.
Так приходим к идее монорепозитория. Добавляем новую библиотеку в существующую рабочую область, что решит первые две проблемы:
projects
├╴app
├╴core (@lib/core)
└╴plugin (@lib/plugin)
angular.json
package.json
tsconfig.json
Итак, в основном package.json
находятся все зависимости библиотек, а у @lib/plugin
в зависимостях (peerDependencies
) есть @lib/core
.
Но мы не решили третью проблему, вносить зависимые изменения всё ещё неудобно, к тому же возникает путаница с адресами: в приложении мы импортируем что-то из core
по-прежнему по пути 'projects/...'
, а в зависимой библиотеке уже пишем from '@lib/core'
, неизменно будут ошибки, и IDE тут не поможет, скорее наоборот будет вставлять автоматически импорты не так, как вам надо. То есть у вас будет первая библиотека как в виде исходников, так и в node_modules
.
Решить проблемы поможет файл tsconfig.json
, где пишем следующее:
{
"compilerOptions": {
"paths": {
"@lib/core": ["projects/core/public-api"],
"@lib/core/*": ["projects/core/*"],
"@lib/plugin": ["projects/plugin/public-api"],
"@lib/plugin/*": ["projects/plugin/*"]
}
}
}
Произошла подмена путей. Теперь мы везде пишем from '@lib/core'
, но на самом деле идём по пути 'project/core/public-api'
, тем самым получаем:
- Единообразие, всегда пишем в том формате, в каком будет писать конечный пользователь;
-
Удобство разработки, когда мы просто вносим где-либо изменения, и
ng serve
их нам показывает, не нужно запаковывать и переустанавливать библиотеки.
Возникает один нюанс: а как же быть со сборкой библиотеки @lib/plugin
, ведь она теперь затянет в себя все исходники core
из projects
, а должна лишь ссылаться на него как на зависимость. А для этого случае просто сбросим пути в соответствующем tsconfig
:
projects
└╴plugin (@lib/plugin)
└╴tsconfig.lib.prod.json
{
"compilerOptions": {
"paths": {
}
}
}
Подразумевается, что все tsconfig
наследуются от корневого.
Теперь это настоящая внешняя зависимость, а значит в процессе сборки нам всё-таки необходимо в node_modules
иметь установленную @lib/core
, но ставить это будем не в корень, а в папку проекта. Причём ограничимся только данным пакетом, сборщик возьмёт его оттуда, а остальное подтянет из корня (поскольку не найдёт нужные зависимости во внутренних node_modules
, начнёт искать у родителей).
cd projects/plugin && npm i @lib/core --no-save
node_modules
└╴@angular
projects
└╴plugin (@lib/plugin)
└╴node_modules
└╴@lib
└╴core
package.json
package.json
Установку пакета не стоит делать вручную, достаточно дописать это в секцию scripts
для package.json
.
Вот и всё, получили удобную систему разработки нескольких библиотек в рамках одного репозитория. В следующей заметке расскажу, как ещё можно разбить билиотеку (уменьшив бандл для конечных пользователей) не создавая дополнительных пакетов.
{{ 'Comments (%count%)' | trans {count:count} }}
{{ 'Comments are closed.' | trans }}