Моно­репозиторий с зависимыми библиотеками

Исходные данные: разрабатываем библиотеку компонентов на Ангуляре. Поехали!

При помощи Ангуляра могут создаваться проекты двух типов: приложения и библиотеки. Как указано выше, нас интересует разработка именно библиотеки. Но в процессе разработки эту библиотеку необходимо где-то тестировать. Соответственно, нам нужно тестовое приложение. Неудобно создавать для этого отдельное рабочее пространство, поэтому положим их рядом, в папку 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'

Пока всё просто. Но спустя некоторое время библиотека начинает разрастаться, появляются специфические крупные блоки, которые нужны не всем пользователям. Поэтому решаем разбить библиотеку на основную функциональность и некие плагины — дополнительные пакеты, которые используют основную библиотеку. Наличие нескольких библиотек может быть удобным и для конечных пользователей в процессе обновления, миграций, фиксации определённой версии части функциональности.

В данной ситуации можно было бы создать новый репозиторий, где будет дополнительная библиотека и также тестовое приложение. Но у данного решения есть ряд минусов:

  1. В тестовых приложениях могут оказаться дублирующие куски кода (например, административная часть, оформление), особенно если эти приложения необходимо заливать на сервер для демонстраций;
  2. Также при деплое тестовых приложений возрастает нагрузка на CI, возникает необходимость выделения различных адресов для разных приложений, что может создать неудобства и для конечных пользователей;
  3. Неудобство разработки, когда мы что-то разрабатываем в зависимой библиотеке, и нам необходимо вносить правки в основной. Выглядело бы это так: переключаемся в основную рабочую область, вносим некие правки, запаковываем библиотеку, затем переключаемся обратно, устанавливаем зависимость, запускаем тестовое приложение, видим что что-то пошло не так, повторяем весь цикл.

Так приходим к идее монорепозитория. Добавляем новую библиотеку в существующую рабочую область, что решит первые две проблемы:

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', тем самым получаем:

  1. Единообразие, всегда пишем в том формате, в каком будет писать конечный пользователь;
  2. Удобство разработки, когда мы просто вносим где-либо изменения, и 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.

Вот и всё, получили удобную систему разработки нескольких библиотек в рамках одного репозитория. В следующей заметке расскажу, как ещё можно разбить билиотеку (уменьшив бандл для конечных пользователей) не создавая дополнительных пакетов.

{{ message }}

{{ 'Comments are closed.' | trans }}